Skip to content

Commit

Permalink
feat(prefer-immutable-types): allow overriding options based on where…
Browse files Browse the repository at this point in the history
… the type is declared

fix #800
  • Loading branch information
RebeccaStevens committed Apr 22, 2024
1 parent 71364aa commit 414f358
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 84 deletions.
37 changes: 37 additions & 0 deletions docs/rules/prefer-immutable-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,24 @@ type Options = {
ReadonlyDeep?: Array<Array<{ pattern: string; replace: string }>>;
Immutable?: Array<Array<{ pattern: string; replace: string }>>;
};

overrides?: Array<{
match:
| {
from: "file";
path?: string;
}
| {
from: "lib";
}
| {
from: "package";
package?: string;
}
| TypeDeclarationSpecifier[];
options: Omit<Options, "overrides">;
disable: boolean;
}>;
};
```

Expand Down Expand Up @@ -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.
210 changes: 126 additions & 84 deletions src/rules/prefer-immutable-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import { Immutability } from "is-immutable-type";

import {
type IgnoreClassesOption,
type OverridableOptions,
getCoreOptions,
ignoreClassesOptionSchema,
shouldIgnoreClasses,
shouldIgnoreInFunction,
shouldIgnorePattern,
} 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 {
Expand Down Expand Up @@ -55,7 +58,8 @@ export const fullName = `${ruleNameScope}/${name}`;
type RawEnforcement =
| Exclude<Immutability | keyof typeof Immutability, "Unknown" | "Mutable">
| "None"
| false;
| false
| undefined;

type Option = IgnoreClassesOption & {
enforcement: RawEnforcement;
Expand All @@ -64,6 +68,20 @@ type Option = IgnoreClassesOption & {
ignoreTypePattern?: string[] | string;
};

type CoreOptions = Option & {
parameters?: Partial<Option> | RawEnforcement;
returnTypes?: Partial<Option> | RawEnforcement;
variables?:
| Partial<
Option & {
ignoreInFunctions?: boolean;
}
>
| RawEnforcement;
fixer?: FixerConfigRawMap;
suggestions?: SuggestionConfigRawMap;
};

type FixerConfigRaw = {
pattern: string;
replace: string;
Expand Down Expand Up @@ -93,21 +111,7 @@ type SuggestionsConfig = FixerConfig[];
/**
* The options this rule can take.
*/
type Options = [
Option & {
parameters?: Partial<Option> | RawEnforcement;
returnTypes?: Partial<Option> | RawEnforcement;
variables?:
| Partial<
Option & {
ignoreInFunctions?: boolean;
}
>
| RawEnforcement;
fixer?: FixerConfigRawMap;
suggestions?: SuggestionConfigRawMap;
},
];
type Options = [OverridableOptions<CoreOptions>];

/**
* The enum options for the level of enforcement.
Expand Down Expand Up @@ -211,51 +215,75 @@ const suggestionsSchema: JSONSchema4 = {
},
};

const coreOptionsPropertiesSchema: JSONSchema4ObjectSchema["properties"] = {
parameters: optionSchema,
returnTypes: optionSchema,
variables: {
oneOf: [
{
type: "object",
properties: deepmerge(optionExpandedSchema, {
ignoreInFunctions: {
type: "boolean",
},
}),
additionalProperties: false,
},
{
type: ["string", "number", "boolean"],
enum: enforcementEnumOptions,
},
],
},
fixer: {
type: "object",
properties: {
ReadonlyShallow: fixerSchema,
ReadonlyDeep: fixerSchema,
Immutable: fixerSchema,
},
additionalProperties: false,
},
suggestions: {
type: "object",
properties: {
ReadonlyShallow: suggestionsSchema,
ReadonlyDeep: suggestionsSchema,
Immutable: suggestionsSchema,
},
additionalProperties: false,
},
};

/**
* The schema for the rule options.
*/
const schema: JSONSchema4[] = [
{
type: "object",
properties: deepmerge(optionExpandedSchema, {
parameters: optionSchema,
returnTypes: optionSchema,
variables: {
oneOf: [
{
type: "object",
properties: deepmerge(optionExpandedSchema, {
ignoreInFunctions: {
type: "boolean",
},
}),
additionalProperties: false,
},
{
type: ["string", "number", "boolean"],
enum: enforcementEnumOptions,
properties: deepmerge(optionExpandedSchema, coreOptionsPropertiesSchema, {
overrides: {
type: "array",
items: {
type: "object",
properties: {
specifiers: typeSpecifiersSchema,
options: {
type: "object",
properties: deepmerge(
optionExpandedSchema,
coreOptionsPropertiesSchema,
),
additionalProperties: false,
},
disable: {
type: "boolean",
},
},
],
},
fixer: {
type: "object",
properties: {
ReadonlyShallow: fixerSchema,
ReadonlyDeep: fixerSchema,
Immutable: fixerSchema,
},
additionalProperties: false,
},
suggestions: {
type: "object",
properties: {
ReadonlyShallow: suggestionsSchema,
ReadonlyDeep: suggestionsSchema,
Immutable: suggestionsSchema,
additionalProperties: false,
},
additionalProperties: false,
},
}),
} satisfies JSONSchema4ObjectSchema["properties"]),
additionalProperties: false,
},
];
Expand Down Expand Up @@ -398,7 +426,7 @@ function getConfiguredSuggestionFixers(
* Get the level of enforcement from the raw value given.
*/
function parseEnforcement(rawEnforcement: RawEnforcement) {
return rawEnforcement === "None"
return rawEnforcement === "None" || rawEnforcement === undefined
? false
: typeof rawEnforcement === "string"
? Immutability[rawEnforcement]
Expand Down Expand Up @@ -454,35 +482,32 @@ function parseSuggestionsConfigs(
function getParameterTypeViolations(
node: ESFunctionType,
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
options: Readonly<Options>,
options: Readonly<CoreOptions>,
): Descriptor[] {
const [optionsObject] = options;
const {
parameters: rawOption,
fixer: rawFixerConfig,
suggestions: rawSuggestionsConfigs,
} = optionsObject;
} = options;
const {
enforcement: rawEnforcement,
ignoreInferredTypes,
ignoreClasses,
ignoreNamePattern,
ignoreTypePattern,
} = {
ignoreInferredTypes: optionsObject.ignoreInferredTypes,
ignoreClasses: optionsObject.ignoreClasses,
ignoreNamePattern: optionsObject.ignoreNamePattern,
ignoreTypePattern: optionsObject.ignoreTypePattern,
ignoreInferredTypes: options.ignoreInferredTypes,
ignoreClasses: options.ignoreClasses,
ignoreNamePattern: options.ignoreNamePattern,
ignoreTypePattern: options.ignoreTypePattern,
...(typeof rawOption === "object"
? rawOption
: {
enforcement: rawOption,
}),
};

const enforcement = parseEnforcement(
rawEnforcement ?? optionsObject.enforcement,
);
const enforcement = parseEnforcement(rawEnforcement ?? options.enforcement);
if (
enforcement === false ||
shouldIgnoreClasses(node, context, ignoreClasses)
Expand Down Expand Up @@ -592,31 +617,28 @@ function getParameterTypeViolations(
function getReturnTypeViolations(
node: ESFunctionType,
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
options: Readonly<Options>,
options: Readonly<CoreOptions>,
): Descriptor[] {
const [optionsObject] = options;
const {
returnTypes: rawOption,
fixer: rawFixerConfig,
suggestions: rawSuggestionsConfigs,
} = optionsObject;
} = options;
const {
enforcement: rawEnforcement,
ignoreInferredTypes,
ignoreClasses,
ignoreNamePattern,
ignoreTypePattern,
} = {
ignoreInferredTypes: optionsObject.ignoreInferredTypes,
ignoreClasses: optionsObject.ignoreClasses,
ignoreNamePattern: optionsObject.ignoreNamePattern,
ignoreTypePattern: optionsObject.ignoreTypePattern,
ignoreInferredTypes: options.ignoreInferredTypes,
ignoreClasses: options.ignoreClasses,
ignoreNamePattern: options.ignoreNamePattern,
ignoreTypePattern: options.ignoreTypePattern,
...(typeof rawOption === "object" ? rawOption : { enforcement: rawOption }),
};

const enforcement = parseEnforcement(
rawEnforcement ?? optionsObject.enforcement,
);
const enforcement = parseEnforcement(rawEnforcement ?? options.enforcement);

if (
enforcement === false ||
Expand Down Expand Up @@ -743,10 +765,19 @@ function checkFunction(
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
options: Readonly<Options>,
): RuleResult<keyof typeof errorMessages, Options> {
const descriptors = [
...getParameterTypeViolations(node, context, options),
...getReturnTypeViolations(node, context, options),
];
const optionsToUse = getCoreOptions<CoreOptions, Options>(
node,
context,
options,
);

const descriptors =
optionsToUse === null
? []
: [
...getParameterTypeViolations(node, context, optionsToUse),
...getReturnTypeViolations(node, context, optionsToUse),
];

return {
context,
Expand All @@ -762,13 +793,24 @@ function checkVariable(
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
options: Readonly<Options>,
): RuleResult<keyof typeof errorMessages, Options> {
const [optionsObject] = options;
const optionsToUse = getCoreOptions<CoreOptions, Options>(
node,
context,
options,
);

if (optionsToUse === null) {
return {
context,
descriptors: [],
};
}

const {
variables: rawOption,
fixer: rawFixerConfig,
suggestions: rawSuggestionsConfigs,
} = optionsObject;
} = optionsToUse;
const {
enforcement: rawEnforcement,
ignoreInferredTypes,
Expand All @@ -777,16 +819,16 @@ function checkVariable(
ignoreTypePattern,
ignoreInFunctions,
} = {
ignoreInferredTypes: optionsObject.ignoreInferredTypes,
ignoreClasses: optionsObject.ignoreClasses,
ignoreNamePattern: optionsObject.ignoreNamePattern,
ignoreTypePattern: optionsObject.ignoreTypePattern,
ignoreInferredTypes: optionsToUse.ignoreInferredTypes,
ignoreClasses: optionsToUse.ignoreClasses,
ignoreNamePattern: optionsToUse.ignoreNamePattern,
ignoreTypePattern: optionsToUse.ignoreTypePattern,
ignoreInFunctions: false,
...(typeof rawOption === "object" ? rawOption : { enforcement: rawOption }),
};

const enforcement = parseEnforcement(
rawEnforcement ?? optionsObject.enforcement,
rawEnforcement ?? optionsToUse.enforcement,
);

if (
Expand Down
Loading

0 comments on commit 414f358

Please sign in to comment.