Skip to content

Commit 8dd704a

Browse files
feat(prefer-immutable-types): allow overriding options based on where the type is declared
fix #800
1 parent cbcc388 commit 8dd704a

File tree

5 files changed

+461
-209
lines changed

5 files changed

+461
-209
lines changed

docs/rules/prefer-immutable-types.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,37 @@ type Options = {
244244
ReadonlyDeep?: Array<Array<{ pattern: string; replace: string }>>;
245245
Immutable?: Array<Array<{ pattern: string; replace: string }>>;
246246
};
247+
248+
overrides?: Array<{
249+
match: Array<
250+
| {
251+
from: "file";
252+
path?: string;
253+
name?: string | string[];
254+
pattern?: RegExp | RegExp[];
255+
ignoreName?: string | string[];
256+
ignorePattern?: RegExp | RegExp[];
257+
}
258+
| {
259+
from: "lib";
260+
name?: string | string[];
261+
pattern?: RegExp | RegExp[];
262+
ignoreName?: string | string[];
263+
ignorePattern?: RegExp | RegExp[];
264+
}
265+
| {
266+
from: "package";
267+
package?: string;
268+
name?: string | string[];
269+
pattern?: RegExp | RegExp[];
270+
ignoreName?: string | string[];
271+
ignorePattern?: RegExp | RegExp[];
272+
}
273+
>;
274+
options: Omit<Options, "overrides">;
275+
inherit?: boolean;
276+
disable: boolean;
277+
}>;
247278
};
248279
```
249280

@@ -475,3 +506,29 @@ It allows for the ability to ignore violations based on the identifier (name) of
475506

476507
This option takes a `RegExp` string or an array of `RegExp` strings.
477508
It allows for the ability to ignore violations based on the type (as written, with whitespace removed) of the node in question.
509+
510+
### `overrides`
511+
512+
Allows for applying overrides to the options based on where the type is defined.
513+
This can be used to override the settings for types coming from 3rd party libraries.
514+
515+
Note: Only the first matching override will be used.
516+
517+
#### `overrides[n].specifiers`
518+
519+
A specifier, or an array of specifiers to match the function type against.
520+
521+
In the case of reference types, both the type and its generics will be recursively checked.
522+
If any of them match, the specifier will be considered a match.
523+
524+
#### `overrides[n].options`
525+
526+
The options to use when a specifiers matches.
527+
528+
#### `overrides[n].inherit`
529+
530+
Inherit the root options? Default is `true`.
531+
532+
#### `overrides[n].disable`
533+
534+
If true, when a specifier matches, this rule will not be applied to the matching node.

src/options/overrides.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import assert from "node:assert/strict";
2+
13
import { type TSESTree } from "@typescript-eslint/utils";
24
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
35
import { deepmerge } from "deepmerge-ts";
4-
import typeMatchesSpecifier from "ts-declaration-location";
6+
import typeMatchesSpecifier, {
7+
type TypeDeclarationSpecifier,
8+
} from "ts-declaration-location";
9+
import { type Program, type Type, type TypeNode } from "typescript";
510

611
import { getTypeDataOfNode } from "#eslint-plugin-functional/utils/rule";
712
import {
@@ -108,13 +113,30 @@ export function getCoreOptions<
108113
}
109114

110115
const [type, typeNode] = getTypeDataOfNode(node, context);
116+
return getCoreOptionsForType(type, typeNode, context, options);
117+
}
118+
119+
export function getCoreOptionsForType<
120+
CoreOptions extends object,
121+
Options extends Readonly<OverridableOptions<CoreOptions>>,
122+
>(
123+
type: Type,
124+
typeNode: TypeNode | null,
125+
context: Readonly<RuleContext<string, unknown[]>>,
126+
options: Readonly<Options>,
127+
): CoreOptions | null {
128+
const program = context.sourceCode.parserServices?.program ?? undefined;
129+
if (program === undefined) {
130+
return options;
131+
}
132+
111133
const found = options.overrides?.find((override) =>
112134
(Array.isArray(override.specifiers)
113135
? override.specifiers
114136
: [override.specifiers]
115137
).some(
116138
(specifier) =>
117-
typeMatchesSpecifier(program, specifier, type) &&
139+
typeMatchesSpecifierDeep(program, specifier, type) &&
118140
(specifier.include === undefined ||
119141
specifier.include.length === 0 ||
120142
typeMatchesPattern(
@@ -139,3 +161,25 @@ export function getCoreOptions<
139161

140162
return options;
141163
}
164+
165+
function typeMatchesSpecifierDeep(
166+
program: Program,
167+
specifier: TypeDeclarationSpecifier,
168+
type: Type,
169+
) {
170+
const stack = [type];
171+
// eslint-disable-next-line functional/no-loop-statements -- best to do this iteratively.
172+
while (stack.length > 0) {
173+
const t = stack.pop() ?? assert.fail();
174+
175+
if (typeMatchesSpecifier(program, specifier, t)) {
176+
return true;
177+
}
178+
179+
if (t.aliasTypeArguments !== undefined) {
180+
stack.push(...t.aliasTypeArguments);
181+
}
182+
}
183+
184+
return false;
185+
}

0 commit comments

Comments
 (0)