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 (#826)
  • Loading branch information
RebeccaStevens committed Aug 5, 2024
1 parent defd713 commit c04e425
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 64 deletions.
55 changes: 55 additions & 0 deletions docs/rules/immutable-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,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 @@ -179,3 +209,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.
188 changes: 124 additions & 64 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 "#/options";
import { isExpected, ruleNameScope } from "#/utils/misc";
import {
Expand All @@ -24,6 +28,7 @@ import {
createRule,
getTypeOfNode,
} from "#/utils/rule";
import { overridableOptionsSchema } from "#/utils/schemas";
import {
findRootIdentifier,
isDefinedByMutableVariable,
Expand Down Expand Up @@ -51,62 +56,61 @@ export const name = "immutable-data";
*/
export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${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 Down Expand Up @@ -218,16 +222,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 @@ -283,16 +301,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 @@ -347,16 +379,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 @@ -414,7 +460,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 @@ -486,16 +532,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 @@ -515,7 +575,7 @@ function checkCallExpression(
};
}

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

// Array mutation?
if (
Expand Down Expand Up @@ -606,9 +666,9 @@ function checkCallExpression(
}

// 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, {
AssignmentExpression: checkAssignmentExpression,
UnaryExpression: checkUnaryExpression,
Expand Down

0 comments on commit c04e425

Please sign in to comment.