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 15, 2024
1 parent 20f7137 commit 78839d6
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 140 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.
1 change: 1 addition & 0 deletions src/options/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./ignore";
export * from "./overrides";
68 changes: 68 additions & 0 deletions src/options/overrides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { type TSESTree } from "@typescript-eslint/utils";
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
import typeMatchesSpecifier, {
type TypeDeclarationSpecifier,
} from "ts-declaration-location";

import { getTypeOfNode } from "../utils/rule";

/**
* Options that can be overridden.
*/
export type OverridableOptions<CoreOptions> = CoreOptions & {
overrides?: Array<
{
specifiers: TypeDeclarationSpecifier | TypeDeclarationSpecifier[];
} & (
| {
options: CoreOptions;
disable?: false;
}
| {
disable: true;
}
)
>;
};

/**
* Get the core options to use, taking into account overrides.
*
* @throws when there is a configuration error.
*/
export function getCoreOptions<
CoreOptions extends object,
Options extends readonly [Readonly<OverridableOptions<CoreOptions>>],
>(
node: TSESTree.Node,
context: Readonly<RuleContext<string, Options>>,
options: Readonly<Options>,
): CoreOptions | null {
const [optionsObject] = options;

const program = context.sourceCode.parserServices?.program ?? undefined;
if (program === undefined) {
return optionsObject;
}

const type = getTypeOfNode(node, context);
const found = optionsObject.overrides?.find((override) =>
(Array.isArray(override.specifiers)
? override.specifiers
: [override.specifiers]
).some((specifier) => typeMatchesSpecifier(program, specifier, type)),
);

if (found !== undefined) {
if (found.disable === true) {
return null;
}
if (found.options === undefined) {
// eslint-disable-next-line functional/no-throw-statements
throw new Error("Configuration error: No options found for override.");
}
return found.options;
}

return optionsObject;
}
65 changes: 9 additions & 56 deletions src/rules/functional-parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,21 @@ import {
} from "@typescript-eslint/utils/json-schema";
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
import { deepmerge } from "deepmerge-ts";
import typeMatchesSpecifier, {
type TypeDeclarationSpecifier,
} from "ts-declaration-location";

import {
getCoreOptions,
ignoreIdentifierPatternOptionSchema,
ignorePrefixSelectorOptionSchema,
shouldIgnorePattern,
type IgnoreIdentifierPatternOption,
type IgnorePrefixSelectorOption,
type OverridableOptions,
} from "#eslint-plugin-functional/options";
import { typeSpecifiersSchema } from "#eslint-plugin-functional/utils/common-schemas";
import { ruleNameScope } from "#eslint-plugin-functional/utils/misc";
import { type ESFunction } from "#eslint-plugin-functional/utils/node-types";
import {
createRuleUsingFunction,
getTypeOfNode,
type NamedCreateRuleCustomMeta,
type RuleResult,
} from "#eslint-plugin-functional/utils/rule";
Expand Down Expand Up @@ -69,23 +67,7 @@ type CoreOptions = IgnoreIdentifierPatternOption &
/**
* The options this rule can take.
*/
type Options = [
CoreOptions & {
overrides?: Array<
{
specifiers: TypeDeclarationSpecifier | TypeDeclarationSpecifier[];
} & (
| {
options: CoreOptions;
disable?: false;
}
| {
disable: true;
}
)
>;
},
];
type Options = [OverridableOptions<CoreOptions>];

const coreOptionsPropertiesSchema: JSONSchema4ObjectSchema["properties"] = {
allowRestParameter: {
Expand Down Expand Up @@ -206,39 +188,6 @@ const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages, Options> = {
schema,
};

/**
* Get the core options to use, taking into account overrides.
*/
function getCoreOptions(
node: TSESTree.Node,
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
options: Readonly<Options>,
): CoreOptions | null {
const [optionsObject] = options;

const program = context.sourceCode.parserServices?.program ?? undefined;
if (program === undefined) {
return optionsObject;
}

const type = getTypeOfNode(node, context);
const found = optionsObject.overrides?.find((override) =>
(Array.isArray(override.specifiers)
? override.specifiers
: [override.specifiers]
).some((specifier) => typeMatchesSpecifier(program, specifier, type)),
);

if (found !== undefined) {
if (found.disable === true) {
return null;
}
return found.options;
}

return optionsObject;
}

/**
* Get the rest parameter violations.
*/
Expand Down Expand Up @@ -313,7 +262,11 @@ function checkFunction(
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
options: Readonly<Options>,
): RuleResult<keyof typeof errorMessages, Options> {
const optionsToUse = getCoreOptions(node, context, options);
const optionsToUse = getCoreOptions<CoreOptions, Options>(
node,
context,
options,
);

if (optionsToUse === null) {
return {
Expand Down Expand Up @@ -359,7 +312,7 @@ function checkIdentifier(
const optionsToUse =
functionNode === null
? options[0]
: getCoreOptions(functionNode, context, options);
: getCoreOptions<CoreOptions, Options>(functionNode, context, options);

if (optionsToUse === null) {
return {
Expand Down
Loading

0 comments on commit 78839d6

Please sign in to comment.