Skip to content

Commit ffaaa6b

Browse files
part: set up overridable options
1 parent 0dc490d commit ffaaa6b

File tree

7 files changed

+373
-8
lines changed

7 files changed

+373
-8
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@
7373
"@typescript-eslint/utils": "^7.6.0",
7474
"deepmerge-ts": "^5.1.0",
7575
"escape-string-regexp": "^4.0.0",
76-
"is-immutable-type": "^3.1.0",
77-
"ts-api-utils": "^1.3.0"
76+
"is-immutable-type": "^4.0.0",
77+
"ts-api-utils": "^1.3.0",
78+
"ts-declaration-location": "^1.0.0"
7879
},
7980
"devDependencies": {
8081
"@babel/eslint-parser": "7.24.1",

pnpm-lock.yaml

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/options/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./ignore";
2+
export * from "./overrides";

src/options/overrides.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { type TSESTree } from "@typescript-eslint/utils";
2+
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
3+
import { deepmerge } from "deepmerge-ts";
4+
import typeMatchesSpecifier from "ts-declaration-location";
5+
6+
import { getTypeDataOfNode } from "#eslint-plugin-functional/utils/rule";
7+
import {
8+
type TypeSpecifier,
9+
typeMatchesPattern,
10+
} from "#eslint-plugin-functional/utils/type-specifier";
11+
12+
/**
13+
* Options that can be overridden.
14+
*/
15+
export type OverridableOptions<CoreOptions> = CoreOptions & {
16+
overrides?: Array<
17+
{
18+
specifiers: TypeSpecifier | TypeSpecifier[];
19+
} & (
20+
| {
21+
options: CoreOptions;
22+
inherit?: boolean;
23+
disable?: false;
24+
}
25+
| {
26+
disable: true;
27+
}
28+
)
29+
>;
30+
};
31+
32+
/**
33+
* Get the core options to use, taking into account overrides.
34+
*/
35+
export function getCoreOptions<
36+
CoreOptions extends object,
37+
Options extends readonly [Readonly<OverridableOptions<CoreOptions>>],
38+
>(
39+
node: TSESTree.Node,
40+
context: Readonly<RuleContext<string, Options>>,
41+
options: Readonly<Options>,
42+
): CoreOptions | null {
43+
const [optionsObject] = options;
44+
45+
const program = context.sourceCode.parserServices?.program ?? undefined;
46+
if (program === undefined) {
47+
return optionsObject;
48+
}
49+
50+
const [type, typeNode] = getTypeDataOfNode(node, context);
51+
const found = optionsObject.overrides?.find((override) =>
52+
(Array.isArray(override.specifiers)
53+
? override.specifiers
54+
: [override.specifiers]
55+
).some(
56+
(specifier) =>
57+
typeMatchesSpecifier(program, specifier, type) &&
58+
(specifier.include === undefined ||
59+
specifier.include.length === 0 ||
60+
typeMatchesPattern(
61+
program,
62+
type,
63+
typeNode,
64+
specifier.include,
65+
specifier.exclude,
66+
)),
67+
),
68+
);
69+
70+
if (found !== undefined) {
71+
if (found.disable === true) {
72+
return null;
73+
}
74+
if (found.inherit !== false) {
75+
return deepmerge(optionsObject, found.options) as CoreOptions;
76+
}
77+
return found.options;
78+
}
79+
80+
return optionsObject;
81+
}

src/utils/rule.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import assert from "node:assert/strict";
2+
13
import { type TSESTree } from "@typescript-eslint/utils";
24
import {
35
type NamedCreateRuleMeta,
@@ -21,6 +23,8 @@ import { getImmutabilityOverrides } from "#eslint-plugin-functional/settings";
2123
import { __VERSION__ } from "#eslint-plugin-functional/utils/constants";
2224
import { type ESFunction } from "#eslint-plugin-functional/utils/node-types";
2325

26+
import { typeMatchesPattern } from "./type-specifier";
27+
2428
/**
2529
* Any custom rule meta properties.
2630
*/
@@ -187,10 +191,49 @@ export function getTypeOfNode<Context extends RuleContext<string, BaseOptions>>(
187191
node: TSESTree.Node,
188192
context: Context,
189193
): Type {
194+
assert(ts !== undefined);
195+
190196
const { esTreeNodeToTSNodeMap } = getParserServices(context);
191197

192198
const tsNode = esTreeNodeToTSNodeMap.get(node);
193-
return getTypeOfTSNode(tsNode, context);
199+
const typedNode = ts.isIdentifier(tsNode) ? tsNode.parent : tsNode;
200+
return getTypeOfTSNode(typedNode, context);
201+
}
202+
203+
/**
204+
* Get the type of the the given node.
205+
*/
206+
export function getTypeNodeOfNode<
207+
Context extends RuleContext<string, BaseOptions>,
208+
>(node: TSESTree.Node, context: Context): TypeNode | null {
209+
assert(ts !== undefined);
210+
211+
const { esTreeNodeToTSNodeMap } = getParserServices(context);
212+
213+
const tsNode = esTreeNodeToTSNodeMap.get(node);
214+
const typedNode = (
215+
ts.isIdentifier(tsNode) ? tsNode.parent : tsNode
216+
) as TSNode & { type?: TypeNode };
217+
return typedNode.type ?? null;
218+
}
219+
220+
/**
221+
* Get the type of the the given node.
222+
*/
223+
export function getTypeDataOfNode<
224+
Context extends RuleContext<string, BaseOptions>,
225+
>(node: TSESTree.Node, context: Context): [Type, TypeNode | null] {
226+
assert(ts !== undefined);
227+
228+
const { esTreeNodeToTSNodeMap } = getParserServices(context);
229+
230+
const tsNode = esTreeNodeToTSNodeMap.get(node);
231+
const typedNode = (
232+
ts.isIdentifier(tsNode) ? tsNode.parent : tsNode
233+
) as TSNode & {
234+
type?: TypeNode;
235+
};
236+
return [getTypeOfTSNode(typedNode, context), typedNode.type ?? null];
194237
}
195238

196239
/**
@@ -275,6 +318,7 @@ export function getTypeImmutabilityOfNode<
275318
// Don't use the global cache in testing environments as it may cause errors when switching between different config options.
276319
process.env["NODE_ENV"] !== "test",
277320
maxImmutability,
321+
typeMatchesPattern,
278322
);
279323
}
280324

src/utils/schemas.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {
2+
type JSONSchema4,
3+
type JSONSchema4ObjectSchema,
4+
} from "@typescript-eslint/utils/json-schema";
5+
6+
const typeSpecifierPatternSchemaProperties: JSONSchema4ObjectSchema["properties"] =
7+
{
8+
name: schemaInstanceOrInstanceArray({
9+
type: "string",
10+
}),
11+
pattern: schemaInstanceOrInstanceArray({
12+
type: "string",
13+
}),
14+
ignoreName: schemaInstanceOrInstanceArray({
15+
type: "string",
16+
}),
17+
ignorePattern: schemaInstanceOrInstanceArray({
18+
type: "string",
19+
}),
20+
};
21+
22+
const typeSpecifierSchema: JSONSchema4 = {
23+
oneOf: [
24+
{
25+
type: "object",
26+
properties: {
27+
...typeSpecifierPatternSchemaProperties,
28+
from: {
29+
type: "string",
30+
enum: ["file"],
31+
},
32+
path: {
33+
type: "string",
34+
},
35+
},
36+
additionalProperties: false,
37+
},
38+
{
39+
type: "object",
40+
properties: {
41+
...typeSpecifierPatternSchemaProperties,
42+
from: {
43+
type: "string",
44+
enum: ["lib"],
45+
},
46+
},
47+
additionalProperties: false,
48+
},
49+
{
50+
type: "object",
51+
properties: {
52+
...typeSpecifierPatternSchemaProperties,
53+
from: {
54+
type: "string",
55+
enum: ["package"],
56+
},
57+
package: {
58+
type: "string",
59+
},
60+
},
61+
additionalProperties: false,
62+
},
63+
],
64+
};
65+
66+
export const typeSpecifiersSchema: JSONSchema4 =
67+
schemaInstanceOrInstanceArray(typeSpecifierSchema);
68+
69+
export function schemaInstanceOrInstanceArray(
70+
items: JSONSchema4,
71+
): NonNullable<JSONSchema4ObjectSchema["properties"]>[string] {
72+
return {
73+
oneOf: [
74+
items,
75+
{
76+
type: "array",
77+
items,
78+
},
79+
],
80+
};
81+
}

0 commit comments

Comments
 (0)