/
rule.ts
173 lines (155 loc) · 4.99 KB
/
rule.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import type { TSESLint, TSESTree } from "@typescript-eslint/experimental-utils";
import { ESLintUtils } from "@typescript-eslint/experimental-utils";
import type { Rule } from "eslint";
import type { Node, Type } from "typescript";
import { shouldIgnore } from "~/common/ignore-options";
import { version } from "~/package.json";
import { isTypeReadonly } from "./upstream/eslint-typescript/isTypeReadonly";
export type BaseOptions = object;
// "url" will be set automatically.
export type RuleMetaDataDocs = Omit<TSESLint.RuleMetaDataDocs, "url">;
// "docs.url" will be set automatically.
export type RuleMetaData<MessageIds extends string> = Omit<
TSESLint.RuleMetaData<MessageIds>,
"docs"
> & {
readonly docs: RuleMetaDataDocs;
};
export type RuleContext<
MessageIds extends string,
Options extends BaseOptions
> = TSESLint.RuleContext<MessageIds, readonly [Options]>;
export type RuleResult<
MessageIds extends string,
Options extends BaseOptions
> = {
readonly context: RuleContext<MessageIds, Options>;
readonly descriptors: ReadonlyArray<TSESLint.ReportDescriptor<MessageIds>>;
};
export type RuleFunctionsMap<
MessageIds extends string,
Options extends BaseOptions
> = {
readonly [K in keyof TSESLint.RuleListener]: (
node: NonNullable<TSESLint.RuleListener[K]> extends TSESLint.RuleFunction<
infer U
>
? U
: never,
context: RuleContext<MessageIds, Options>,
options: Options
) => RuleResult<MessageIds, Options>;
};
// This function can't be functional as it needs to interact with 3rd-party
// libraries that aren't functional.
/* eslint-disable functional/no-return-void, functional/no-conditional-statement, functional/no-expression-statement */
/**
* Create a function that processes common options and then runs the given
* check.
*/
function checkNode<
MessageIds extends string,
Context extends RuleContext<MessageIds, BaseOptions>,
Node extends TSESTree.Node,
Options extends BaseOptions
>(
check: (
node: Node,
context: Context,
options: Options
) => RuleResult<MessageIds, Options>,
context: Context,
options: Options
): (node: Node) => void {
return (node: Node) => {
if (!shouldIgnore(node, context, options)) {
const result = check(node, context, options);
// eslint-disable-next-line functional/no-loop-statement -- can't really be avoided.
for (const descriptor of result.descriptors) {
result.context.report(descriptor);
}
}
};
}
/* eslint-enable functional/no-return-void, functional/no-conditional-statement, functional/no-expression-statement */
/**
* Create a rule.
*/
export function createRule<
MessageIds extends string,
Options extends BaseOptions
>(
name: string,
meta: RuleMetaData<MessageIds>,
defaultOptions: Options,
ruleFunctionsMap: RuleFunctionsMap<MessageIds, Options>
): Rule.RuleModule {
return ESLintUtils.RuleCreator(
(name) =>
`https://github.com/jonaskello/eslint-plugin-functional/blob/v${version}/docs/rules/${name}.md`
)({
name,
meta,
defaultOptions: [defaultOptions],
create: (
context: TSESLint.RuleContext<MessageIds, readonly [Options]>,
[options]: readonly [Options]
) =>
Object.fromEntries(
Object.entries(ruleFunctionsMap).map(([nodeSelector, ruleFunction]) => [
nodeSelector,
checkNode(ruleFunction, context, options),
])
),
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
} as any) as any;
}
/**
* Get the type of the the given node.
*/
export function getTypeOfNode<Context extends RuleContext<string, BaseOptions>>(
node: TSESTree.Node,
context: Context
): Type | null {
const { parserServices } = context;
if (
parserServices === undefined ||
parserServices.program === undefined ||
parserServices.esTreeNodeToTSNodeMap === undefined
) {
return null;
}
const checker = parserServices.program.getTypeChecker();
const nodeType = checker.getTypeAtLocation(
parserServices.esTreeNodeToTSNodeMap.get(node)
);
const constrained = checker.getBaseConstraintOfType(nodeType);
return constrained ?? nodeType;
}
export function isReadonly<Context extends RuleContext<string, BaseOptions>>(
node: TSESTree.Node,
context: Context
): boolean {
const { parserServices } = context;
if (parserServices === undefined || parserServices.program === undefined) {
return false;
}
const checker = parserServices.program.getTypeChecker();
const type = getTypeOfNode(node, context);
// return ESLintUtils.isTypeReadonly(checker, type!);
return isTypeReadonly(checker, type!);
}
/**
* Get the es tree node from the given ts node.
*/
export function getESTreeNode<Context extends RuleContext<string, BaseOptions>>(
node: Node,
context: Context
): TSESTree.Node | null {
const { parserServices } = context;
return parserServices === undefined ||
parserServices.program === undefined ||
parserServices.tsNodeToESTreeNodeMap === undefined
? null
: parserServices.tsNodeToESTreeNodeMap.get(node);
}