/
RuleTester.ts
129 lines (114 loc) · 3.82 KB
/
RuleTester.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
import * as path from 'path';
import * as TSESLint from '../ts-eslint';
const parser = '@typescript-eslint/parser';
type RuleTesterConfig = Omit<TSESLint.RuleTesterConfig, 'parser'> & {
parser: typeof parser;
};
class RuleTester extends TSESLint.RuleTester {
readonly #options: RuleTesterConfig;
// as of eslint 6 you have to provide an absolute path to the parser
// but that's not as clean to type, this saves us trying to manually enforce
// that contributors require.resolve everything
constructor(options: RuleTesterConfig) {
super({
...options,
parserOptions: {
...options.parserOptions,
warnOnUnsupportedTypeScriptVersion:
options.parserOptions?.warnOnUnsupportedTypeScriptVersion ?? false,
},
parser: require.resolve(options.parser),
});
this.#options = options;
// make sure that the parser doesn't hold onto file handles between tests
// on linux (i.e. our CI env), there can be very a limited number of watch handles available
if (typeof afterAll !== 'undefined') {
afterAll(() => {
try {
// instead of creating a hard dependency, just use a soft require
// a bit weird, but if they're using this tooling, it'll be installed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
require(parser).clearCaches();
} catch {
// ignored
}
});
}
}
private getFilename(options?: TSESLint.ParserOptions): string {
if (options) {
const filename = `file.ts${options.ecmaFeatures?.jsx ? 'x' : ''}`;
if (options.project) {
return path.join(
options.tsconfigRootDir != null
? options.tsconfigRootDir
: process.cwd(),
filename,
);
}
return filename;
} else if (this.#options.parserOptions) {
return this.getFilename(this.#options.parserOptions);
}
return 'file.ts';
}
// as of eslint 6 you have to provide an absolute path to the parser
// If you don't do that at the test level, the test will fail somewhat cryptically...
// This is a lot more explicit
run<TMessageIds extends string, TOptions extends Readonly<unknown[]>>(
name: string,
rule: TSESLint.RuleModule<TMessageIds, TOptions>,
testsReadonly: TSESLint.RunTests<TMessageIds, TOptions>,
): void {
const errorMessage = `Do not set the parser at the test level unless you want to use a parser other than ${parser}`;
const tests = { ...testsReadonly };
// standardize the valid tests as objects
tests.valid = tests.valid.map(test => {
if (typeof test === 'string') {
return {
code: test,
};
}
return test;
});
tests.valid = tests.valid.map(test => {
if (typeof test !== 'string') {
if (test.parser === parser) {
throw new Error(errorMessage);
}
if (!test.filename) {
return {
...test,
filename: this.getFilename(test.parserOptions),
};
}
}
return test;
});
tests.invalid = tests.invalid.map(test => {
if (test.parser === parser) {
throw new Error(errorMessage);
}
if (!test.filename) {
return {
...test,
filename: this.getFilename(test.parserOptions),
};
}
return test;
});
super.run(name, rule, tests);
}
}
/**
* Simple no-op tag to mark code samples as "should not format with prettier"
* for the internal/plugin-test-formatting lint rule
*/
function noFormat(strings: TemplateStringsArray, ...keys: string[]): string {
const lastIndex = strings.length - 1;
return (
strings.slice(0, lastIndex).reduce((p, s, i) => p + s + keys[i], '') +
strings[lastIndex]
);
}
export { noFormat, RuleTester };