Skip to content

Commit 42d36b1

Browse files
authored
feat: support ESLint v9 (#185)
* ci: test against ESLint v9 * feat: support ESLint v9 * test: support ESLint v9 rule tester * test: move Jest config into dedicated file
1 parent d0cde6a commit 42d36b1

12 files changed

+308
-152
lines changed

.eslintrc.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ module.exports = {
9292
},
9393
},
9494
{
95-
files: ['src/**/*', 'dangerfile.ts'],
95+
files: ['src/**/*', 'dangerfile.ts', './jest.config.ts'],
9696
parserOptions: {
9797
sourceType: 'module',
9898
},

.github/workflows/nodejs.yml

+10-3
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,14 @@ jobs:
7272
fail-fast: false
7373
matrix:
7474
node-version: [14.x, 16.x, 18.x, 19.x, 20.x, 21.x]
75-
eslint-version: [7, 8]
75+
eslint-version: [7, 8, 9]
76+
exclude:
77+
# eslint@9 doesn't support node@16
78+
- node-version: 16.x
79+
eslint-version: 9
80+
# eslint@9 doesn't support node@14
81+
- node-version: 14.x
82+
eslint-version: 9
7683
runs-on: ubuntu-latest
7784

7885
steps:
@@ -92,11 +99,11 @@ jobs:
9299
yarn add --dev eslint@${{ matrix.eslint-version }}
93100
- name: run tests
94101
# only collect coverage on eslint versions that support dynamic import
95-
run: yarn test --coverage ${{ matrix.eslint-version >= 8 }}
102+
run: yarn test --coverage ${{ matrix.eslint-version == 8 }}
96103
env:
97104
CI: true
98105
- uses: codecov/codecov-action@v3
99-
if: ${{ matrix.eslint-version >= 8 }}
106+
if: ${{ matrix.eslint-version == 8 }}
100107
test-ubuntu:
101108
uses: ./.github/workflows/test.yml
102109
needs: prepare-yarn-cache-ubuntu

jest.config.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { version as eslintVersion } from 'eslint/package.json';
2+
import type { Config } from 'jest';
3+
import * as semver from 'semver';
4+
5+
const config = {
6+
clearMocks: true,
7+
restoreMocks: true,
8+
resetMocks: true,
9+
10+
coverageThreshold: {
11+
global: {
12+
branches: 100,
13+
functions: 100,
14+
lines: 100,
15+
statements: 100,
16+
},
17+
},
18+
19+
projects: [
20+
{
21+
displayName: 'test',
22+
testPathIgnorePatterns: [
23+
'<rootDir>/lib/.*',
24+
'<rootDir>/src/rules/__tests__/test-utils.ts',
25+
],
26+
coveragePathIgnorePatterns: ['/node_modules/'],
27+
},
28+
{
29+
displayName: 'lint',
30+
runner: 'jest-runner-eslint',
31+
testMatch: ['<rootDir>/**/*.{js,ts}'],
32+
testPathIgnorePatterns: ['<rootDir>/lib/.*'],
33+
coveragePathIgnorePatterns: ['/node_modules/'],
34+
},
35+
],
36+
} satisfies Config;
37+
38+
if (semver.major(eslintVersion) >= 9) {
39+
config.projects = config.projects.filter(
40+
({ displayName }) => displayName !== 'lint',
41+
);
42+
}
43+
44+
export default config;

package.json

+3-32
Original file line numberDiff line numberDiff line change
@@ -69,35 +69,6 @@
6969
"@semantic-release/github"
7070
]
7171
},
72-
"jest": {
73-
"coverageThreshold": {
74-
"global": {
75-
"branches": 100,
76-
"functions": 100,
77-
"lines": 100,
78-
"statements": 100
79-
}
80-
},
81-
"projects": [
82-
{
83-
"displayName": "test",
84-
"testPathIgnorePatterns": [
85-
"<rootDir>/lib/.*",
86-
"<rootDir>/src/rules/__tests__/test-utils.ts"
87-
]
88-
},
89-
{
90-
"displayName": "lint",
91-
"runner": "jest-runner-eslint",
92-
"testMatch": [
93-
"<rootDir>/**/*.{js,ts}"
94-
],
95-
"testPathIgnorePatterns": [
96-
"<rootDir>/lib/.*"
97-
]
98-
}
99-
]
100-
},
10172
"resolutions": {
10273
"@typescript-eslint/experimental-utils": "^5.0.0"
10374
},
@@ -123,7 +94,7 @@
12394
"babel-jest": "^29.0.0",
12495
"babel-plugin-replace-ts-export-assignment": "^0.0.2",
12596
"dedent": "^1.0.0",
126-
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0",
97+
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
12798
"eslint-config-prettier": "^9.0.0",
12899
"eslint-doc-generator": "^1.0.0",
129100
"eslint-plugin-eslint-comments": "^3.1.2",
@@ -143,12 +114,12 @@
143114
"prettier": "^3.0.0",
144115
"rimraf": "^5.0.0",
145116
"semantic-release": "^23.0.0",
146-
"semver": "^7.0.0",
117+
"semver": "^7.6.0",
147118
"ts-node": "^10.9.1",
148119
"typescript": "^5.0.0"
149120
},
150121
"peerDependencies": {
151-
"eslint": "^7.0.0 || ^8.0.0"
122+
"eslint": "^7.0.0 || ^8.0.0 || ^9.0.0"
152123
},
153124
"packageManager": "[email protected]",
154125
"engines": {

src/rules/__tests__/prefer-to-be-array.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { TSESLint } from '@typescript-eslint/utils';
1+
import type { TSESLint } from '@typescript-eslint/utils';
22
import rule, { type MessageIds, type Options } from '../prefer-to-be-array';
3-
import { espreeParser } from './test-utils';
3+
import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils';
44

5-
const ruleTester = new TSESLint.RuleTester({
5+
const ruleTester = new RuleTester({
66
parser: espreeParser,
77
parserOptions: {
88
ecmaVersion: 2017,

src/rules/__tests__/prefer-to-be-false.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { TSESLint } from '@typescript-eslint/utils';
21
import rule from '../prefer-to-be-false';
2+
import { FlatCompatRuleTester as RuleTester } from './test-utils';
33

4-
const ruleTester = new TSESLint.RuleTester();
4+
const ruleTester = new RuleTester();
55

66
ruleTester.run('prefer-to-be-false', rule, {
77
valid: [
@@ -58,7 +58,7 @@ ruleTester.run('prefer-to-be-false', rule, {
5858
],
5959
});
6060

61-
new TSESLint.RuleTester({
61+
new RuleTester({
6262
parser: require.resolve('@typescript-eslint/parser'),
6363
}).run('prefer-to-be-false: typescript edition', rule, {
6464
valid: [

src/rules/__tests__/prefer-to-be-object.test.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { TSESLint } from '@typescript-eslint/utils';
1+
import type { TSESLint } from '@typescript-eslint/utils';
22
import rule, { type MessageIds, type Options } from '../prefer-to-be-object';
3+
import { FlatCompatRuleTester as RuleTester } from './test-utils';
34

4-
const ruleTester = new TSESLint.RuleTester();
5+
const ruleTester = new RuleTester();
56

67
// makes ts happy about the dynamic test generation
78
const messageId = 'preferToBeObject' as const;

src/rules/__tests__/prefer-to-be-true.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { TSESLint } from '@typescript-eslint/utils';
21
import rule from '../prefer-to-be-true';
2+
import { FlatCompatRuleTester as RuleTester } from './test-utils';
33

4-
const ruleTester = new TSESLint.RuleTester();
4+
const ruleTester = new RuleTester();
55

66
ruleTester.run('prefer-to-be-true', rule, {
77
valid: [
@@ -58,7 +58,7 @@ ruleTester.run('prefer-to-be-true', rule, {
5858
],
5959
});
6060

61-
new TSESLint.RuleTester({
61+
new RuleTester({
6262
parser: require.resolve('@typescript-eslint/parser'),
6363
}).run('prefer-to-be-true: typescript edition', rule, {
6464
valid: [

src/rules/__tests__/prefer-to-have-been-called-once.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { TSESLint } from '@typescript-eslint/utils';
21
import rule from '../prefer-to-have-been-called-once';
2+
import { FlatCompatRuleTester as RuleTester } from './test-utils';
33

4-
const ruleTester = new TSESLint.RuleTester();
4+
const ruleTester = new RuleTester();
55

66
ruleTester.run('prefer-to-have-been-called-once', rule, {
77
valid: [
@@ -38,7 +38,7 @@ ruleTester.run('prefer-to-have-been-called-once', rule, {
3838
],
3939
});
4040

41-
new TSESLint.RuleTester({
41+
new RuleTester({
4242
parser: require.resolve('@typescript-eslint/parser'),
4343
}).run('prefer-to-have-been-called-once: typescript edition', rule, {
4444
valid: [

src/rules/__tests__/test-utils.ts

+157
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,163 @@
11
import { createRequire } from 'module';
2+
import { TSESLint } from '@typescript-eslint/utils';
3+
import { version as eslintVersion } from 'eslint/package.json';
4+
import * as semver from 'semver';
25

36
const require = createRequire(__filename);
47
const eslintRequire = createRequire(require.resolve('eslint'));
58

69
export const espreeParser = eslintRequire.resolve('espree');
10+
11+
export const usingFlatConfig = semver.major(eslintVersion) >= 9;
12+
13+
export class FlatCompatRuleTester extends TSESLint.RuleTester {
14+
public constructor(testerConfig?: TSESLint.RuleTesterConfig) {
15+
super(FlatCompatRuleTester._flatCompat(testerConfig));
16+
}
17+
18+
public override run<
19+
TMessageIds extends string,
20+
TOptions extends Readonly<unknown[]>,
21+
>(
22+
ruleName: string,
23+
rule: TSESLint.RuleModule<TMessageIds, TOptions>,
24+
tests: TSESLint.RunTests<TMessageIds, TOptions>,
25+
) {
26+
super.run(ruleName, rule, {
27+
valid: tests.valid.map(t => FlatCompatRuleTester._flatCompat(t)),
28+
invalid: tests.invalid.map(t => FlatCompatRuleTester._flatCompat(t)),
29+
});
30+
}
31+
32+
/* istanbul ignore next */
33+
private static _flatCompat<
34+
T extends
35+
| undefined
36+
| RuleTesterConfig
37+
| string
38+
| TSESLint.ValidTestCase<unknown[]>
39+
| TSESLint.InvalidTestCase<string, unknown[]>,
40+
>(config: T): T {
41+
if (!config || !usingFlatConfig || typeof config === 'string') {
42+
return config;
43+
}
44+
45+
const obj: FlatConfig.Config & {
46+
languageOptions: FlatConfig.LanguageOptions & {
47+
parserOptions: FlatConfig.ParserOptions;
48+
};
49+
} = {
50+
languageOptions: { parserOptions: {} },
51+
};
52+
53+
for (const [key, value] of Object.entries(config)) {
54+
if (key === 'parser') {
55+
obj.languageOptions.parser = require(value);
56+
57+
continue;
58+
}
59+
60+
if (key === 'parserOptions') {
61+
for (const [option, val] of Object.entries(value)) {
62+
if (option === 'ecmaVersion' || option === 'sourceType') {
63+
// @ts-expect-error: TS thinks the value could the opposite type of whatever option is
64+
obj.languageOptions[option] = val as FlatConfig.LanguageOptions[
65+
| 'ecmaVersion'
66+
| 'sourceType'];
67+
68+
continue;
69+
}
70+
71+
obj.languageOptions.parserOptions[option] = val;
72+
}
73+
74+
continue;
75+
}
76+
77+
obj[key as keyof typeof obj] = value;
78+
}
79+
80+
return obj as unknown as T;
81+
}
82+
}
83+
84+
type RuleTesterConfig = TSESLint.RuleTesterConfig | FlatConfig.Config;
85+
86+
export declare namespace FlatConfig {
87+
type EcmaVersion = TSESLint.EcmaVersion;
88+
type ParserOptions = TSESLint.ParserOptions;
89+
type SourceType = TSESLint.SourceType | 'commonjs';
90+
interface LanguageOptions {
91+
/**
92+
* The version of ECMAScript to support.
93+
* May be any year (i.e., `2022`) or version (i.e., `5`).
94+
* Set to `"latest"` for the most recent supported version.
95+
* @default "latest"
96+
*/
97+
ecmaVersion?: EcmaVersion;
98+
/**
99+
* An object specifying additional objects that should be added to the global scope during linting.
100+
*/
101+
globals?: unknown;
102+
/**
103+
* An object containing a `parse()` method or a `parseForESLint()` method.
104+
* @default
105+
* ```
106+
* // https://github.com/eslint/espree
107+
* require('espree')
108+
* ```
109+
*/
110+
parser?: unknown;
111+
/**
112+
* An object specifying additional options that are passed directly to the parser.
113+
* The available options are parser-dependent.
114+
*/
115+
parserOptions?: ParserOptions;
116+
/**
117+
* The type of JavaScript source code.
118+
* Possible values are `"script"` for traditional script files, `"module"` for ECMAScript modules (ESM), and `"commonjs"` for CommonJS files.
119+
* @default
120+
* ```
121+
* // for `.js` and `.mjs` files
122+
* "module"
123+
* // for `.cjs` files
124+
* "commonjs"
125+
* ```
126+
*/
127+
sourceType?: SourceType;
128+
}
129+
interface Config {
130+
/**
131+
* An array of glob patterns indicating the files that the configuration object should apply to.
132+
* If not specified, the configuration object applies to all files matched by any other configuration object.
133+
*/
134+
files?: string[];
135+
/**
136+
* An array of glob patterns indicating the files that the configuration object should not apply to.
137+
* If not specified, the configuration object applies to all files matched by files.
138+
*/
139+
ignores?: string[];
140+
/**
141+
* An object containing settings related to how JavaScript is configured for linting.
142+
*/
143+
languageOptions?: LanguageOptions;
144+
/**
145+
* An object containing settings related to the linting process.
146+
*/
147+
linterOptions?: unknown;
148+
/**
149+
* An object containing a name-value mapping of plugin names to plugin objects.
150+
* When `files` is specified, these plugins are only available to the matching files.
151+
*/
152+
plugins?: unknown;
153+
/**
154+
* An object containing the configured rules.
155+
* When `files` or `ignores` are specified, these rule configurations are only available to the matching files.
156+
*/
157+
rules?: unknown;
158+
/**
159+
* An object containing name-value pairs of information that should be available to all rules.
160+
*/
161+
settings?: unknown;
162+
}
163+
}

0 commit comments

Comments
 (0)