Skip to content

Commit 6f42f1e

Browse files
part: set up overridable options
1 parent cbaf1a9 commit 6f42f1e

File tree

8 files changed

+478
-7
lines changed

8 files changed

+478
-7
lines changed

knip.jsonc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "node_modules/knip/schema-jsonc.json",
33
"entry": ["src/index.ts!", "cz-adapter/index.js", "tests/**/*.test.ts"],
44
"project": ["src/**/*.ts!", "cz-adapter/**/*.{js,ts}", "tests/**/*.{js,ts}"],
5-
"ignore": ["src/utils/conditional-imports/esm/**/*"],
5+
"ignore": ["src/utils/conditional-imports/esm/**/*", "src/utils/schemas.ts"],
66
"ignoreDependencies": [
77
"@stylistic/eslint-plugin",
88
"@rebeccastevens/eslint-config",

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@
8080
"deepmerge-ts": "^7.1.0",
8181
"escape-string-regexp": "^4.0.0",
8282
"is-immutable-type": "^4.0.0",
83-
"ts-api-utils": "^1.3.0"
83+
"ts-api-utils": "^1.3.0",
84+
"ts-declaration-location": "^1.0.3"
8485
},
8586
"devDependencies": {
8687
"@babel/eslint-parser": "7.24.8",

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: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import assert from "node:assert/strict";
2+
3+
import { type TSESTree } from "@typescript-eslint/utils";
4+
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
5+
import { deepmerge } from "deepmerge-ts";
6+
import typeMatchesSpecifier, {
7+
type TypeDeclarationSpecifier,
8+
} from "ts-declaration-location";
9+
import { type Program, type Type, type TypeNode } from "typescript";
10+
11+
import { getTypeDataOfNode } from "#/utils/rule";
12+
import {
13+
type RawTypeSpecifier,
14+
type TypeSpecifier,
15+
typeMatchesPattern,
16+
} from "#/utils/type-specifier";
17+
18+
/**
19+
* Options that can be overridden.
20+
*/
21+
export type OverridableOptions<CoreOptions> = CoreOptions & {
22+
overrides?: Array<
23+
{
24+
specifiers: TypeSpecifier | TypeSpecifier[];
25+
} & (
26+
| {
27+
options: CoreOptions;
28+
inherit?: boolean;
29+
disable?: false;
30+
}
31+
| {
32+
disable: true;
33+
}
34+
)
35+
>;
36+
};
37+
38+
export type RawOverridableOptions<CoreOptions> = CoreOptions & {
39+
overrides?: Array<{
40+
specifiers?: RawTypeSpecifier | RawTypeSpecifier[];
41+
options?: CoreOptions;
42+
inherit?: boolean;
43+
disable?: boolean;
44+
}>;
45+
};
46+
47+
export function upgradeRawOverridableOptions<CoreOptions>(
48+
raw: Readonly<RawOverridableOptions<CoreOptions>>,
49+
): OverridableOptions<CoreOptions> {
50+
return {
51+
...raw,
52+
overrides:
53+
raw.overrides?.map((override) => ({
54+
...override,
55+
specifiers:
56+
override.specifiers === undefined
57+
? []
58+
: Array.isArray(override.specifiers)
59+
? override.specifiers.map(upgradeRawTypeSpecifier)
60+
: [upgradeRawTypeSpecifier(override.specifiers)],
61+
})) ?? [],
62+
} as OverridableOptions<CoreOptions>;
63+
}
64+
65+
function upgradeRawTypeSpecifier(raw: RawTypeSpecifier): TypeSpecifier {
66+
const { ignoreName, ignorePattern, name, pattern, ...rest } = raw;
67+
68+
const names = name === undefined ? [] : Array.isArray(name) ? name : [name];
69+
70+
const patterns = (
71+
pattern === undefined ? [] : Array.isArray(pattern) ? pattern : [pattern]
72+
).map((p) => new RegExp(p, "u"));
73+
74+
const ignoreNames =
75+
ignoreName === undefined
76+
? []
77+
: Array.isArray(ignoreName)
78+
? ignoreName
79+
: [ignoreName];
80+
81+
const ignorePatterns = (
82+
ignorePattern === undefined
83+
? []
84+
: Array.isArray(ignorePattern)
85+
? ignorePattern
86+
: [ignorePattern]
87+
).map((p) => new RegExp(p, "u"));
88+
89+
const include = [...names, ...patterns];
90+
const exclude = [...ignoreNames, ...ignorePatterns];
91+
92+
return {
93+
...rest,
94+
include,
95+
exclude,
96+
};
97+
}
98+
99+
/**
100+
* Get the core options to use, taking into account overrides.
101+
*/
102+
export function getCoreOptions<
103+
CoreOptions extends object,
104+
Options extends Readonly<OverridableOptions<CoreOptions>>,
105+
>(
106+
node: TSESTree.Node,
107+
context: Readonly<RuleContext<string, unknown[]>>,
108+
options: Readonly<Options>,
109+
): CoreOptions | null {
110+
const program = context.sourceCode.parserServices?.program ?? undefined;
111+
if (program === undefined) {
112+
return options;
113+
}
114+
115+
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+
133+
const found = options.overrides?.find((override) =>
134+
(Array.isArray(override.specifiers)
135+
? override.specifiers
136+
: [override.specifiers]
137+
).some(
138+
(specifier) =>
139+
typeMatchesSpecifierDeep(program, specifier, type) &&
140+
(specifier.include === undefined ||
141+
specifier.include.length === 0 ||
142+
typeMatchesPattern(
143+
program,
144+
type,
145+
typeNode,
146+
specifier.include,
147+
specifier.exclude,
148+
)),
149+
),
150+
);
151+
152+
if (found !== undefined) {
153+
if (found.disable === true) {
154+
return null;
155+
}
156+
if (found.inherit !== false) {
157+
return deepmerge(options, found.options) as CoreOptions;
158+
}
159+
return found.options;
160+
}
161+
162+
return options;
163+
}
164+
165+
function typeMatchesSpecifierDeep(
166+
program: Program,
167+
specifier: TypeDeclarationSpecifier,
168+
type: Type,
169+
) {
170+
const m_stack = [type];
171+
// eslint-disable-next-line functional/no-loop-statements -- best to do this iteratively.
172+
while (m_stack.length > 0) {
173+
const t = m_stack.pop() ?? assert.fail();
174+
175+
if (typeMatchesSpecifier(program, specifier, t)) {
176+
return true;
177+
}
178+
179+
if (t.aliasTypeArguments !== undefined) {
180+
m_stack.push(...t.aliasTypeArguments);
181+
}
182+
}
183+
184+
return false;
185+
}

src/utils/rule.ts

Lines changed: 39 additions & 0 deletions
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,
@@ -22,6 +24,8 @@ import { getImmutabilityOverrides } from "#/settings";
2224
import { __VERSION__ } from "#/utils/constants";
2325
import { type ESFunction } from "#/utils/node-types";
2426

27+
import { typeMatchesPattern } from "./type-specifier";
28+
2529
/**
2630
* Any custom rule meta properties.
2731
*/
@@ -187,12 +191,46 @@ 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);
193199
return getTypeOfTSNode(tsNode, context);
194200
}
195201

202+
/**
203+
* Get the type of the the given node.
204+
*/
205+
export function getTypeNodeOfNode<
206+
Context extends RuleContext<string, BaseOptions>,
207+
>(node: TSESTree.Node, context: Context): TypeNode | null {
208+
assert(ts !== undefined);
209+
210+
const { esTreeNodeToTSNodeMap } = getParserServices(context);
211+
212+
const tsNode = esTreeNodeToTSNodeMap.get(node) as TSNode & {
213+
type?: TypeNode;
214+
};
215+
return tsNode.type ?? null;
216+
}
217+
218+
/**
219+
* Get the type of the the given node.
220+
*/
221+
export function getTypeDataOfNode<
222+
Context extends RuleContext<string, BaseOptions>,
223+
>(node: TSESTree.Node, context: Context): [Type, TypeNode | null] {
224+
assert(ts !== undefined);
225+
226+
const { esTreeNodeToTSNodeMap } = getParserServices(context);
227+
228+
const tsNode = esTreeNodeToTSNodeMap.get(node) as TSNode & {
229+
type?: TypeNode;
230+
};
231+
return [getTypeOfTSNode(tsNode, context), tsNode.type ?? null];
232+
}
233+
196234
/**
197235
* Get the type of the the given ts node.
198236
*/
@@ -280,6 +318,7 @@ export function getTypeImmutabilityOfNode<
280318
// Don't use the global cache in testing environments as it may cause errors when switching between different config options.
281319
process.env["NODE_ENV"] !== "test",
282320
maxImmutability,
321+
typeMatchesPattern,
283322
);
284323
}
285324

0 commit comments

Comments
 (0)