Skip to content

Commit 2dd1638

Browse files
authored
feat(experimental-utils): expose our RuleTester extension (#1948)
1 parent 383f931 commit 2dd1638

File tree

4 files changed

+120
-116
lines changed

4 files changed

+120
-116
lines changed

Diff for: packages/eslint-plugin-internal/tests/RuleTester.ts

+7-19
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,10 @@
1-
import { TSESLint, ESLintUtils } from '@typescript-eslint/experimental-utils';
1+
import { ESLintUtils } from '@typescript-eslint/experimental-utils';
2+
import path from 'path';
23

3-
const { batchedSingleLineTests } = ESLintUtils;
4-
5-
const parser = '@typescript-eslint/parser';
6-
7-
type RuleTesterConfig = Omit<TSESLint.RuleTesterConfig, 'parser'> & {
8-
parser: typeof parser;
9-
};
10-
class RuleTester extends TSESLint.RuleTester {
11-
// as of eslint 6 you have to provide an absolute path to the parser
12-
// but that's not as clean to type, this saves us trying to manually enforce
13-
// that contributors require.resolve everything
14-
constructor(options: RuleTesterConfig) {
15-
super({
16-
...options,
17-
parser: require.resolve(options.parser),
18-
});
19-
}
4+
function getFixturesRootDir(): string {
5+
return path.join(__dirname, 'fixtures');
206
}
217

22-
export { RuleTester, batchedSingleLineTests };
8+
const { batchedSingleLineTests, RuleTester } = ESLintUtils;
9+
10+
export { RuleTester, batchedSingleLineTests, getFixturesRootDir };

Diff for: packages/eslint-plugin/tests/RuleTester.ts

+3-97
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,10 @@
1-
import { TSESLint, ESLintUtils } from '@typescript-eslint/experimental-utils';
2-
import { clearCaches } from '@typescript-eslint/parser';
1+
import { ESLintUtils } from '@typescript-eslint/experimental-utils';
32
import * as path from 'path';
43

5-
const parser = '@typescript-eslint/parser';
6-
7-
type RuleTesterConfig = Omit<TSESLint.RuleTesterConfig, 'parser'> & {
8-
parser: typeof parser;
9-
};
10-
class RuleTester extends TSESLint.RuleTester {
11-
// as of eslint 6 you have to provide an absolute path to the parser
12-
// but that's not as clean to type, this saves us trying to manually enforce
13-
// that contributors require.resolve everything
14-
constructor(private readonly options: RuleTesterConfig) {
15-
super({
16-
...options,
17-
parser: require.resolve(options.parser),
18-
});
19-
}
20-
private getFilename(options?: TSESLint.ParserOptions): string {
21-
if (options) {
22-
const filename = `file.ts${
23-
options.ecmaFeatures && options.ecmaFeatures.jsx ? 'x' : ''
24-
}`;
25-
if (options.project) {
26-
return path.join(getFixturesRootDir(), filename);
27-
}
28-
29-
return filename;
30-
} else if (this.options.parserOptions) {
31-
return this.getFilename(this.options.parserOptions);
32-
}
33-
34-
return 'file.ts';
35-
}
36-
37-
// as of eslint 6 you have to provide an absolute path to the parser
38-
// If you don't do that at the test level, the test will fail somewhat cryptically...
39-
// This is a lot more explicit
40-
run<TMessageIds extends string, TOptions extends Readonly<unknown[]>>(
41-
name: string,
42-
rule: TSESLint.RuleModule<TMessageIds, TOptions>,
43-
tests: TSESLint.RunTests<TMessageIds, TOptions>,
44-
): void {
45-
const errorMessage = `Do not set the parser at the test level unless you want to use a parser other than ${parser}`;
46-
47-
// standardize the valid tests as objects
48-
tests.valid = tests.valid.map(test => {
49-
if (typeof test === 'string') {
50-
return {
51-
code: test,
52-
};
53-
}
54-
return test;
55-
});
56-
57-
tests.valid.forEach(test => {
58-
if (typeof test !== 'string') {
59-
if (test.parser === parser) {
60-
throw new Error(errorMessage);
61-
}
62-
if (!test.filename) {
63-
test.filename = this.getFilename(test.parserOptions);
64-
}
65-
}
66-
});
67-
tests.invalid.forEach(test => {
68-
if (test.parser === parser) {
69-
throw new Error(errorMessage);
70-
}
71-
if (!test.filename) {
72-
test.filename = this.getFilename(test.parserOptions);
73-
}
74-
});
75-
76-
super.run(name, rule, tests);
77-
}
78-
}
79-
804
function getFixturesRootDir(): string {
81-
return path.join(process.cwd(), 'tests/fixtures/');
5+
return path.join(__dirname, 'fixtures');
826
}
837

84-
const { batchedSingleLineTests } = ESLintUtils;
85-
86-
// make sure that the parser doesn't hold onto file handles between tests
87-
// on linux (i.e. our CI env), there can be very a limited number of watch handles available
88-
afterAll(() => {
89-
clearCaches();
90-
});
91-
92-
/**
93-
* Simple no-op tag to mark code samples as "should not format with prettier"
94-
* for the internal/plugin-test-formatting lint rule
95-
*/
96-
function noFormat(strings: TemplateStringsArray, ...keys: string[]): string {
97-
const lastIndex = strings.length - 1;
98-
return (
99-
strings.slice(0, lastIndex).reduce((p, s, i) => p + s + keys[i], '') +
100-
strings[lastIndex]
101-
);
102-
}
8+
const { batchedSingleLineTests, RuleTester, noFormat } = ESLintUtils;
1039

10410
export { batchedSingleLineTests, getFixturesRootDir, noFormat, RuleTester };
+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import * as TSESLint from '../ts-eslint';
2+
import * as path from 'path';
3+
4+
const parser = '@typescript-eslint/parser';
5+
6+
type RuleTesterConfig = Omit<TSESLint.RuleTesterConfig, 'parser'> & {
7+
parser: typeof parser;
8+
};
9+
10+
class RuleTester extends TSESLint.RuleTester {
11+
// as of eslint 6 you have to provide an absolute path to the parser
12+
// but that's not as clean to type, this saves us trying to manually enforce
13+
// that contributors require.resolve everything
14+
constructor(private readonly options: RuleTesterConfig) {
15+
super({
16+
...options,
17+
parser: require.resolve(options.parser),
18+
});
19+
20+
// make sure that the parser doesn't hold onto file handles between tests
21+
// on linux (i.e. our CI env), there can be very a limited number of watch handles available
22+
afterAll(() => {
23+
try {
24+
// instead of creating a hard dependency, just use a soft require
25+
// a bit weird, but if they're using this tooling, it'll be installed
26+
require(parser).clearCaches();
27+
} catch {
28+
// ignored
29+
}
30+
});
31+
}
32+
private getFilename(options?: TSESLint.ParserOptions): string {
33+
if (options) {
34+
const filename = `file.ts${
35+
options.ecmaFeatures && options.ecmaFeatures.jsx ? 'x' : ''
36+
}`;
37+
if (options.project) {
38+
return path.join(
39+
options.tsconfigRootDir != null
40+
? options.tsconfigRootDir
41+
: process.cwd(),
42+
filename,
43+
);
44+
}
45+
46+
return filename;
47+
} else if (this.options.parserOptions) {
48+
return this.getFilename(this.options.parserOptions);
49+
}
50+
51+
return 'file.ts';
52+
}
53+
54+
// as of eslint 6 you have to provide an absolute path to the parser
55+
// If you don't do that at the test level, the test will fail somewhat cryptically...
56+
// This is a lot more explicit
57+
run<TMessageIds extends string, TOptions extends Readonly<unknown[]>>(
58+
name: string,
59+
rule: TSESLint.RuleModule<TMessageIds, TOptions>,
60+
tests: TSESLint.RunTests<TMessageIds, TOptions>,
61+
): void {
62+
const errorMessage = `Do not set the parser at the test level unless you want to use a parser other than ${parser}`;
63+
64+
// standardize the valid tests as objects
65+
tests.valid = tests.valid.map(test => {
66+
if (typeof test === 'string') {
67+
return {
68+
code: test,
69+
};
70+
}
71+
return test;
72+
});
73+
74+
tests.valid.forEach(test => {
75+
if (typeof test !== 'string') {
76+
if (test.parser === parser) {
77+
throw new Error(errorMessage);
78+
}
79+
if (!test.filename) {
80+
test.filename = this.getFilename(test.parserOptions);
81+
}
82+
}
83+
});
84+
tests.invalid.forEach(test => {
85+
if (test.parser === parser) {
86+
throw new Error(errorMessage);
87+
}
88+
if (!test.filename) {
89+
test.filename = this.getFilename(test.parserOptions);
90+
}
91+
});
92+
93+
super.run(name, rule, tests);
94+
}
95+
}
96+
97+
/**
98+
* Simple no-op tag to mark code samples as "should not format with prettier"
99+
* for the internal/plugin-test-formatting lint rule
100+
*/
101+
function noFormat(strings: TemplateStringsArray, ...keys: string[]): string {
102+
const lastIndex = strings.length - 1;
103+
return (
104+
strings.slice(0, lastIndex).reduce((p, s, i) => p + s + keys[i], '') +
105+
strings[lastIndex]
106+
);
107+
}
108+
109+
export { noFormat, RuleTester };

Diff for: packages/experimental-utils/src/eslint-utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export * from './applyDefault';
22
export * from './batchedSingleLineTests';
33
export * from './getParserServices';
44
export * from './RuleCreator';
5+
export * from './RuleTester';
56
export * from './deepMerge';

0 commit comments

Comments
 (0)