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 May 6, 2024
1 parent cbcc388 commit 8dd704a
Show file tree
Hide file tree
Showing 5 changed files with 461 additions and 209 deletions.
57 changes: 57 additions & 0 deletions docs/rules/prefer-immutable-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,37 @@ type Options = {
ReadonlyDeep?: Array<Array<{ pattern: string; replace: string }>>;
Immutable?: Array<Array<{ pattern: string; replace: 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 @@ -475,3 +506,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 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.

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.
48 changes: 46 additions & 2 deletions src/options/overrides.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import assert from "node:assert/strict";

import { type TSESTree } from "@typescript-eslint/utils";
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
import { deepmerge } from "deepmerge-ts";
import typeMatchesSpecifier from "ts-declaration-location";
import typeMatchesSpecifier, {
type TypeDeclarationSpecifier,
} from "ts-declaration-location";
import { type Program, type Type, type TypeNode } from "typescript";

import { getTypeDataOfNode } from "#eslint-plugin-functional/utils/rule";
import {
Expand Down Expand Up @@ -108,13 +113,30 @@ export function getCoreOptions<
}

const [type, typeNode] = getTypeDataOfNode(node, context);
return getCoreOptionsForType(type, typeNode, context, options);
}

export function getCoreOptionsForType<
CoreOptions extends object,
Options extends Readonly<OverridableOptions<CoreOptions>>,
>(
type: Type,
typeNode: TypeNode | null,
context: Readonly<RuleContext<string, unknown[]>>,
options: Readonly<Options>,
): CoreOptions | null {
const program = context.sourceCode.parserServices?.program ?? undefined;
if (program === undefined) {
return options;
}

const found = options.overrides?.find((override) =>
(Array.isArray(override.specifiers)
? override.specifiers
: [override.specifiers]
).some(
(specifier) =>
typeMatchesSpecifier(program, specifier, type) &&
typeMatchesSpecifierDeep(program, specifier, type) &&
(specifier.include === undefined ||
specifier.include.length === 0 ||
typeMatchesPattern(
Expand All @@ -139,3 +161,25 @@ export function getCoreOptions<

return options;
}

function typeMatchesSpecifierDeep(
program: Program,
specifier: TypeDeclarationSpecifier,
type: Type,
) {
const stack = [type];
// eslint-disable-next-line functional/no-loop-statements -- best to do this iteratively.
while (stack.length > 0) {
const t = stack.pop() ?? assert.fail();

if (typeMatchesSpecifier(program, specifier, t)) {
return true;
}

if (t.aliasTypeArguments !== undefined) {
stack.push(...t.aliasTypeArguments);
}
}

return false;
}
Loading

0 comments on commit 8dd704a

Please sign in to comment.