Skip to content

Commit

Permalink
feat(immutable-data): allows for applying overrides to the options ba…
Browse files Browse the repository at this point in the history
…sed on the root object's type
  • Loading branch information
RebeccaStevens committed May 6, 2024
1 parent fb2ac2d commit 48b4551
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 75 deletions.
55 changes: 55 additions & 0 deletions docs/rules/immutable-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Options, "overrides">;
inherit?: boolean;
disable: boolean;
}>;
};
```

Expand Down Expand Up @@ -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.
211 changes: 136 additions & 75 deletions src/rules/immutable-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +30,7 @@ import {
createRule,
getTypeOfNode,
} from "#eslint-plugin-functional/utils/rule";
import { overridableOptionsSchema } from "#eslint-plugin-functional/utils/schemas";
import {
findRootIdentifier,
isDefinedByMutableVariable,
Expand Down Expand Up @@ -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<CoreOptions>];
type Options = OverridableOptions<CoreOptions>;

/**
* 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<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 = [
{
ignoreClasses: false,
ignoreImmediateMutation: true,
Expand All @@ -128,18 +132,19 @@ const errorMessages = {
/**
* The meta data for this rule.
*/
const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages, Options> = {
type: "suggestion",
docs: {
category: "No Mutations",
description: "Enforce treating data as immutable.",
recommended: "recommended",
recommendedSeverity: "error",
requiresTypeChecking: true,
},
messages: errorMessages,
schema,
};
const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages, RawOptions> =
{
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.
Expand Down Expand Up @@ -220,16 +225,30 @@ const stringConstructorNewObjectReturningMethods = ["split"];
*/
function checkAssignmentExpression(
node: TSESTree.AssignmentExpression,
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 rootNode = findRootIdentifier(node.left) ?? node.left;
const optionsToUse = getCoreOptions<CoreOptions, Options>(
rootNode,
context,
options,
);

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

const {
ignoreIdentifierPattern,
ignoreAccessorPattern,
ignoreNonConstDeclarations,
ignoreClasses,
} = optionsObject;
} = optionsToUse;

if (
!isMemberExpression(node.left) ||
Expand Down Expand Up @@ -285,16 +304,30 @@ function checkAssignmentExpression(
*/
function checkUnaryExpression(
node: TSESTree.UnaryExpression,
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 rootNode = findRootIdentifier(node.argument) ?? node.argument;
const optionsToUse = getCoreOptions<CoreOptions, Options>(
rootNode,
context,
options,
);

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

const {
ignoreIdentifierPattern,
ignoreAccessorPattern,
ignoreNonConstDeclarations,
ignoreClasses,
} = optionsObject;
} = optionsToUse;

if (
!isMemberExpression(node.argument) ||
Expand Down Expand Up @@ -349,16 +382,30 @@ function checkUnaryExpression(
*/
function checkUpdateExpression(
node: TSESTree.UpdateExpression,
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 rootNode = findRootIdentifier(node.argument) ?? node.argument;
const optionsToUse = getCoreOptions<CoreOptions, Options>(
rootNode,
context,
options,
);

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

const {
ignoreIdentifierPattern,
ignoreAccessorPattern,
ignoreNonConstDeclarations,
ignoreClasses,
} = optionsObject;
} = optionsToUse;

if (
!isMemberExpression(node.argument) ||
Expand Down Expand Up @@ -416,7 +463,7 @@ function checkUpdateExpression(
*/
function isInChainCallAndFollowsNew(
node: TSESTree.Expression,
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
): boolean {
if (isMemberExpression(node)) {
return isInChainCallAndFollowsNew(node.object, context);
Expand Down Expand Up @@ -488,16 +535,30 @@ function isInChainCallAndFollowsNew(
*/
function checkCallExpression(
node: TSESTree.CallExpression,
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 rootNode = findRootIdentifier(node.callee) ?? node.callee;
const optionsToUse = getCoreOptions<CoreOptions, Options>(
rootNode,
context,
options,
);

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

const {
ignoreIdentifierPattern,
ignoreAccessorPattern,
ignoreNonConstDeclarations,
ignoreClasses,
} = optionsObject;
} = optionsToUse;

// Not potential object mutation?
if (
Expand All @@ -517,7 +578,7 @@ function checkCallExpression(
};
}

const { ignoreImmediateMutation } = optionsObject;
const { ignoreImmediateMutation } = optionsToUse;

// Array mutation?
if (
Expand Down Expand Up @@ -608,7 +669,7 @@ function checkCallExpression(
}

// Create the rule.
export const rule = createRule<keyof typeof errorMessages, Options>(
export const rule = createRule<keyof typeof errorMessages, RawOptions>(
name,
meta,
defaultOptions,
Expand Down

0 comments on commit 48b4551

Please sign in to comment.