Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(prefer-immutable-types): allows for applying overrides to the op…
Browse files Browse the repository at this point in the history
…tions based on the type

fix #800
RebeccaStevens committed Aug 3, 2024

Verified

This commit was signed with the committer’s verified signature.
RebeccaStevens Rebecca Stevens
1 parent 2ce165e commit 45f7609
Showing 3 changed files with 332 additions and 193 deletions.
57 changes: 57 additions & 0 deletions docs/rules/prefer-immutable-types.md
Original file line number Diff line number Diff line change
@@ -255,6 +255,37 @@ type Options = {
Array<{ pattern: string; replace: string; message?: 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<Options, "overrides">;
inherit?: boolean;
disable: boolean;
}>;
};
```

@@ -488,3 +519,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 the type's declaration.
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.
1 change: 0 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
@@ -55,7 +55,6 @@ export default rsEslint(
"ts/no-unnecessary-condition": "off",

// Temp
"functional/prefer-immutable-types": "off",
"functional/no-throw-statements": "off",
},
},
467 changes: 275 additions & 192 deletions src/rules/prefer-immutable-types.ts
Original file line number Diff line number Diff line change
@@ -9,13 +9,19 @@
} 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 "#/options";
import { ruleNameScope } from "#/utils/misc";
import { type ESFunctionType } from "#/utils/node-types";
@@ -25,6 +31,7 @@
type RuleResult,
createRule,
getReturnTypesOfFunction,
getTypeDataOfNode,
getTypeImmutabilityOfNode,
getTypeImmutabilityOfType,
isImplementationOfOverload,
@@ -43,6 +50,8 @@
isTSTypePredicate,
} from "#/utils/type-guards";

import { overridableOptionsSchema } from "../utils/schemas";

/**
* The name of this rule.
*/
@@ -56,7 +65,8 @@
type RawEnforcement =
| Exclude<Immutability | keyof typeof Immutability, "Unknown" | "Mutable">
| "None"
| false;
| false
| undefined;

type Option = IgnoreClassesOption & {
enforcement: RawEnforcement;
@@ -65,6 +75,20 @@
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;
@@ -96,21 +120,8 @@
/**
* 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 RawOptions = [RawOverridableOptions<CoreOptions>];
type Options = OverridableOptions<CoreOptions>;

/**
* The enum options for the level of enforcement.
@@ -215,59 +226,59 @@
},
};

/**
* 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",
},
} satisfies JSONSchema4ObjectSchema["properties"]),
additionalProperties: false,
},
{
type: ["string", "number", "boolean"],
enum: enforcementEnumOptions,
},
],
},
fixer: {
const coreOptionsPropertiesSchema: NonNullable<
JSONSchema4ObjectSchema["properties"]
> = deepmerge(optionExpandedSchema, {
parameters: optionSchema,
returnTypes: optionSchema,
variables: {
oneOf: [
{
type: "object",
properties: {
ReadonlyShallow: fixerSchema,
ReadonlyDeep: fixerSchema,
Immutable: fixerSchema,
},
properties: deepmerge(optionExpandedSchema, {
ignoreInFunctions: {
type: "boolean",
},
} satisfies JSONSchema4ObjectSchema["properties"]),
additionalProperties: false,
},
suggestions: {
type: "object",
properties: {
ReadonlyShallow: suggestionsSchema,
ReadonlyDeep: suggestionsSchema,
Immutable: suggestionsSchema,
},
additionalProperties: false,
{
type: ["string", "number", "boolean"],
enum: enforcementEnumOptions,
},
} satisfies JSONSchema4ObjectSchema["properties"]),
],
},
fixer: {
type: "object",
properties: {
ReadonlyShallow: fixerSchema,
ReadonlyDeep: fixerSchema,
Immutable: fixerSchema,
},
additionalProperties: false,
},
suggestions: {
type: "object",
properties: {
ReadonlyShallow: suggestionsSchema,
ReadonlyDeep: suggestionsSchema,
Immutable: suggestionsSchema,
},
additionalProperties: false,
},
} satisfies JSONSchema4ObjectSchema["properties"]);

/**
* The schema for the rule options.
*/
const schema: JSONSchema4[] = [
overridableOptionsSchema(coreOptionsPropertiesSchema),
];

/**
* The default options for the rule.
*/
const defaultOptions: Options = [
const defaultOptions: RawOptions = [
{
enforcement: Immutability.Immutable,
ignoreInferredTypes: false,
@@ -337,7 +348,7 @@

type Descriptor = RuleResult<
keyof typeof errorMessages,
Options
RawOptions
>["descriptors"][number];

type AllFixers = {
@@ -350,7 +361,7 @@
*/
function getAllFixers(
node: TSESTree.Node,
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
fixerConfigs: FixerConfig[] | false,
suggestionsConfigs: SuggestionsConfig[] | false,
): AllFixers {
@@ -422,7 +433,7 @@
* 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]
@@ -433,7 +444,7 @@
* Get the fixer config for the the given enforcement level from the raw config given.
*/
function parseFixerConfigs(
allRawConfigs: Options[0]["fixer"],
allRawConfigs: RawOptions[0]["fixer"],
enforcement: Immutability,
): FixerConfig[] | false {
const key = Immutability[enforcement] as keyof NonNullable<
@@ -454,7 +465,7 @@
* Get the suggestions config for the the given enforcement level from the raw config given.
*/
function parseSuggestionsConfigs(
rawSuggestions: Options[0]["suggestions"],
rawSuggestions: RawOptions[0]["suggestions"],
enforcement: Immutability,
): SuggestionsConfig[] | false {
const key = Immutability[enforcement] as keyof NonNullable<
@@ -477,56 +488,67 @@
*/
function getParameterTypeViolations(
node: ESFunctionType,
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
options: Readonly<Options>,
): Descriptor[] {
const [optionsObject] = options;
const {
parameters: rawOption,
fixer: rawFixerConfig,
suggestions: rawSuggestionsConfigs,
} = optionsObject;
const {
enforcement: rawEnforcement,
ignoreInferredTypes,
ignoreClasses,
ignoreNamePattern,
ignoreTypePattern,
} = {
ignoreInferredTypes: optionsObject.ignoreInferredTypes,
ignoreClasses: optionsObject.ignoreClasses,
ignoreNamePattern: optionsObject.ignoreNamePattern,
ignoreTypePattern: optionsObject.ignoreTypePattern,
...(typeof rawOption === "object"
? rawOption
: {
enforcement: rawOption,
}),
};
return node.params
.map((param): Descriptor | undefined => {
const parameterProperty = isTSParameterProperty(param);
const actualParam = parameterProperty ? param.parameter : param;

const enforcement = parseEnforcement(
rawEnforcement ?? optionsObject.enforcement,
);
if (
enforcement === false ||
shouldIgnoreClasses(node, context, ignoreClasses)
) {
return [];
}
const optionsToUse = getCoreOptions<CoreOptions, Options>(
param,
context,
options,
);

const fixerConfigs = parseFixerConfigs(rawFixerConfig, enforcement);
const suggestionsConfigs = parseSuggestionsConfigs(
rawSuggestionsConfigs,
enforcement,
);
if (optionsToUse === null) {
return undefined;
}

const {
parameters: rawOption,
fixer: rawFixerConfig,
suggestions: rawSuggestionsConfigs,
} = optionsToUse;
const {
enforcement: rawEnforcement,
ignoreInferredTypes,
ignoreClasses,
ignoreNamePattern,
ignoreTypePattern,
} = {
ignoreInferredTypes: optionsToUse.ignoreInferredTypes,
ignoreClasses: optionsToUse.ignoreClasses,
ignoreNamePattern: optionsToUse.ignoreNamePattern,
ignoreTypePattern: optionsToUse.ignoreTypePattern,
...(typeof rawOption === "object"
? rawOption
: {
enforcement: rawOption,
}),
};

const enforcement = parseEnforcement(
rawEnforcement ?? optionsToUse.enforcement,
);
if (
enforcement === false ||
shouldIgnoreClasses(node, context, ignoreClasses)
) {
return undefined;
}

const fixerConfigs = parseFixerConfigs(rawFixerConfig, enforcement);
const suggestionsConfigs = parseSuggestionsConfigs(
rawSuggestionsConfigs,
enforcement,
);

return node.params
.map((param): Descriptor | undefined => {
if (shouldIgnorePattern(param, context, ignoreNamePattern)) {
return undefined;
}

const parameterProperty = isTSParameterProperty(param);
if (parameterProperty && !param.readonly) {
const fix: NonNullable<Descriptor["fix"]> | null = (fixer) =>
fixer.insertTextBefore(param.parameter, "readonly ");
@@ -544,8 +566,6 @@
};
}

const actualParam = parameterProperty ? param.parameter : param;

if (
// inferred types
(ignoreInferredTypes && actualParam.typeAnnotation === undefined) ||
@@ -596,7 +616,7 @@
},
fix,
suggest:
suggestionFixers?.map(({ fix, message }) => ({

Check warning on line 619 in src/rules/prefer-immutable-types.ts

GitHub Actions / lint_js

'fix' is already declared in the upper scope on line 600 column 15
messageId: "userDefined",
data: {
message,
@@ -613,92 +633,124 @@
*/
function getReturnTypeViolations(
node: ESFunctionType,
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
options: Readonly<Options>,
): Descriptor[] {
const [optionsObject] = options;
const {
returnTypes: rawOption,
fixer: rawFixerConfig,
suggestions: rawSuggestionsConfigs,
} = optionsObject;
const {
enforcement: rawEnforcement,
ignoreInferredTypes,
ignoreClasses,
ignoreNamePattern,
ignoreTypePattern,
} = {
ignoreInferredTypes: optionsObject.ignoreInferredTypes,
ignoreClasses: optionsObject.ignoreClasses,
ignoreNamePattern: optionsObject.ignoreNamePattern,
ignoreTypePattern: optionsObject.ignoreTypePattern,
...(typeof rawOption === "object" ? rawOption : { enforcement: rawOption }),
};
function getOptions(type: Type, typeNode: TypeNode | null) {
const optionsToUse = getCoreOptionsForType<CoreOptions, Options>(
type,
typeNode,
context,
options,
);

const enforcement = parseEnforcement(
rawEnforcement ?? optionsObject.enforcement,
);
if (optionsToUse === null) {
return null;
}

if (
enforcement === false ||
(ignoreInferredTypes && node.returnType?.typeAnnotation === undefined) ||
shouldIgnoreClasses(node, context, ignoreClasses) ||
shouldIgnorePattern(node, context, ignoreNamePattern)
) {
return [];
}
const {
returnTypes: rawOption,
fixer: rawFixerConfig,
suggestions: rawSuggestionsConfigs,
} = optionsToUse;
const {
enforcement: rawEnforcement,
ignoreClasses,
ignoreNamePattern,
ignoreTypePattern,
ignoreInferredTypes,
} = {
ignoreClasses: optionsToUse.ignoreClasses,
ignoreNamePattern: optionsToUse.ignoreNamePattern,
ignoreTypePattern: optionsToUse.ignoreTypePattern,
ignoreInferredTypes: optionsToUse.ignoreInferredTypes,
...(typeof rawOption === "object"
? rawOption
: { enforcement: rawOption }),
};

const fixerConfigs = parseFixerConfigs(rawFixerConfig, enforcement);
const suggestionsConfigs = parseSuggestionsConfigs(
rawSuggestionsConfigs,
enforcement,
);
const enforcement = parseEnforcement(
rawEnforcement ?? optionsToUse.enforcement,
);

if (
node.returnType?.typeAnnotation !== undefined &&
!isTSTypePredicate(node.returnType.typeAnnotation)
) {
if (shouldIgnorePattern(node.returnType, context, ignoreTypePattern)) {
return [];
if (
enforcement === false ||
shouldIgnoreClasses(node, context, ignoreClasses) ||
shouldIgnorePattern(node, context, ignoreNamePattern)
) {
return null;
}

const immutability = getTypeImmutabilityOfNode(
node.returnType.typeAnnotation,
context,
const fixerConfigs = parseFixerConfigs(rawFixerConfig, enforcement);
const suggestionsConfigs = parseSuggestionsConfigs(
rawSuggestionsConfigs,
enforcement,
);

if (immutability >= enforcement) {
return {
ignoreTypePattern,
ignoreInferredTypes,
enforcement,
fixerConfigs,
suggestionsConfigs,
};
}

if (node.returnType?.typeAnnotation !== undefined) {
const [type, typeNode] = getTypeDataOfNode(node, context);
const optionsToUse = getOptions(type, typeNode);
if (optionsToUse === null) {
return [];
}

const { fix, suggestionFixers } = getAllFixers(
node.returnType.typeAnnotation,
context,
fixerConfigs,
suggestionsConfigs,
);
const { ignoreTypePattern, enforcement, fixerConfigs, suggestionsConfigs } =
optionsToUse;

return [
{
node: node.returnType,
messageId: "returnType",
data: {
actual: Immutability[immutability],
expected: Immutability[enforcement],
if (
node.returnType?.typeAnnotation !== undefined &&
!isTSTypePredicate(node.returnType.typeAnnotation)
) {
if (shouldIgnorePattern(node.returnType, context, ignoreTypePattern)) {
return [];
}

const immutability = getTypeImmutabilityOfNode(
node.returnType.typeAnnotation,
context,
enforcement,
);

if (immutability >= enforcement) {
return [];
}

const { fix, suggestionFixers } = getAllFixers(
node.returnType.typeAnnotation,
context,
fixerConfigs,
suggestionsConfigs,
);

return [
{
node: node.returnType,
messageId: "returnType",
data: {
actual: Immutability[immutability],
expected: Immutability[enforcement],
},
fix,
suggest:
suggestionFixers?.map(({ fix, message }) => ({

Check warning on line 744 in src/rules/prefer-immutable-types.ts

GitHub Actions / lint_js

'fix' is already declared in the upper scope on line 727 column 15
messageId: "userDefined",
data: {
message,
},
fix,
})) ?? null,
},
fix,
suggest:
suggestionFixers?.map(({ fix, message }) => ({
messageId: "userDefined",
data: {
message,
},
fix,
})) ?? null,
},
];
];
}
}

if (!isFunctionLike(node)) {
@@ -714,8 +766,25 @@
return [];
}

const returnType = returnTypes[0]!;

const optionsToUse = getOptions(
returnType,
(returnType as Type & { node: TypeNode }).node ?? null,
);
if (optionsToUse === null) {
return [];
}

const { ignoreInferredTypes, enforcement, fixerConfigs, suggestionsConfigs } =
optionsToUse;

if (ignoreInferredTypes) {
return [];
}

const immutability = getTypeImmutabilityOfType(
returnTypes[0]!,
returnType,
context,
enforcement,
);
@@ -744,7 +813,7 @@
},
fix,
suggest:
suggestionFixers?.map(({ fix, message }) => ({

Check warning on line 816 in src/rules/prefer-immutable-types.ts

GitHub Actions / lint_js

'fix' is already declared in the upper scope on line 796 column 11
messageId: "userDefined",
data: {
message,
@@ -760,9 +829,11 @@
*/
function checkFunction(
node: ESFunctionType,
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
options: Readonly<Options>,
): RuleResult<keyof typeof errorMessages, Options> {
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
rawOptions: Readonly<RawOptions>,
): RuleResult<keyof typeof errorMessages, RawOptions> {
const options = upgradeRawOverridableOptions(rawOptions[0]);

const descriptors = [
...getParameterTypeViolations(node, context, options),
...getReturnTypeViolations(node, context, options),
@@ -779,16 +850,28 @@
*/
function checkVariable(
node: TSESTree.VariableDeclarator | TSESTree.PropertyDefinition,
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
options: Readonly<Options>,
): RuleResult<keyof typeof errorMessages, Options> {
const [optionsObject] = options;
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
rawOptions: Readonly<RawOptions>,
): RuleResult<keyof typeof errorMessages, RawOptions> {
const options = upgradeRawOverridableOptions(rawOptions[0]);
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,
@@ -797,16 +880,16 @@
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 (
@@ -931,7 +1014,7 @@
data,
fix,
suggest:
suggestionFixers?.map(({ fix, message }) => ({

Check warning on line 1017 in src/rules/prefer-immutable-types.ts

GitHub Actions / lint_js

'fix' is already declared in the upper scope on line 1005 column 38
messageId: "userDefined",
data: {
message,
@@ -944,9 +1027,9 @@
}

// Create the rule.
export const rule: Rule<keyof typeof errorMessages, Options> = createRule<
export const rule: Rule<keyof typeof errorMessages, RawOptions> = createRule<
keyof typeof errorMessages,
Options
RawOptions
>(name, meta, defaultOptions, {
ArrowFunctionExpression: checkFunction,
FunctionDeclaration: checkFunction,

0 comments on commit 45f7609

Please sign in to comment.