/
RuleTester.ts
128 lines (113 loc) · 3.68 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
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
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
require(parser).clearCaches();
} catch {
// ignored
}
});
}
private getFilename(options?: TSESLint.ParserOptions): string {
if (options) {
const filename = `file.ts${
options.ecmaFeatures && 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 };