Skip to content

Commit 3965c8f

Browse files
authored
feat: support ESLint v9 (#355)
1 parent de56a19 commit 3965c8f

20 files changed

+154
-45
lines changed

.github/workflows/validate.yml

+11-1
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,16 @@ jobs:
2222
strategy:
2323
fail-fast: false
2424
matrix:
25-
eslint: [6, 7, 8]
25+
eslint: [6, 7, 8, 9]
2626
node: [12.x, 14.x, 16.x, 18.x, 20.x, 21.x]
2727
testing-library-dom: [8, 9, 10]
2828
exclude:
29+
- eslint: 9
30+
node: 12.x
31+
- eslint: 9
32+
node: 14.x
33+
- eslint: 9
34+
node: 16.x
2935
- testing-library-dom: 9
3036
node: 12.x
3137
- testing-library-dom: 10
@@ -49,6 +55,10 @@ jobs:
4955
with:
5056
useLockFile: false
5157

58+
# see https://github.com/npm/cli/issues/7349
59+
- if: ${{ matrix.eslint == 9 }}
60+
run: npm un @typescript-eslint/parser
61+
5262
- name: Install ESLint v${{ matrix.eslint }}
5363
run: npm install --no-save --force eslint@${{ matrix.eslint }}
5464

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,12 @@
5252
"eslint-remote-tester": "^3.0.0",
5353
"eslint-remote-tester-repositories": "^1.0.1",
5454
"kcd-scripts": "^12.0.0",
55+
"semver": "^7.6.0",
5556
"typescript": "^5.1.3"
5657
},
5758
"peerDependencies": {
5859
"@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0",
59-
"eslint": "^6.8.0 || ^7.0.0 || ^8.0.0"
60+
"eslint": "^6.8.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
6061
},
6162
"peerDependenciesMeta": {
6263
"@testing-library/dom": {

src/__tests__/lib/rules/prefer-empty.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
// Requirements
99
//------------------------------------------------------------------------------
1010

11-
import { RuleTester } from "eslint";
12-
import * as rule from "../../../rules/prefer-empty";
11+
import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
12+
import * as rule from '../../../rules/prefer-empty';
1313

1414
//------------------------------------------------------------------------------
1515
// Tests

src/__tests__/lib/rules/prefer-focus.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @author Ben Monro
44
*/
55

6-
import { RuleTester } from "eslint";
6+
import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
77
import * as rule from "../../../rules/prefer-focus";
88

99
const ruleTester = new RuleTester();

src/__tests__/lib/rules/prefer-in-document.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// Requirements
88
//------------------------------------------------------------------------------
99

10-
import { RuleTester } from "eslint";
10+
import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
1111
import * as rule from "../../../rules/prefer-in-document";
1212

1313
//------------------------------------------------------------------------------

src/__tests__/lib/rules/prefer-prefer-to-have-class.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RuleTester } from "eslint";
1+
import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
22
import * as rule from "../../../rules/prefer-to-have-class";
33

44
const errors = [{ messageId: "use-to-have-class" }];

src/__tests__/lib/rules/prefer-to-have-attribute.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// Requirements
99
//------------------------------------------------------------------------------
1010

11-
import { RuleTester } from "eslint";
11+
import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
1212
import * as rule from "../../../rules/prefer-to-have-attribute";
1313

1414
//------------------------------------------------------------------------------

src/__tests__/lib/rules/prefer-to-have-style.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RuleTester } from "eslint";
1+
import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
22
import * as rule from "../../../rules/prefer-to-have-style";
33

44
const errors = [

src/__tests__/lib/rules/prefer-to-have-text-content.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// Requirements
99
//------------------------------------------------------------------------------
1010

11-
import { RuleTester } from "eslint";
11+
import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
1212
import * as rule from "../../../rules/prefer-to-have-text-content";
1313

1414
//------------------------------------------------------------------------------

src/__tests__/lib/rules/prefer-to-have-value.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// Requirements
99
//------------------------------------------------------------------------------
1010

11-
import { RuleTester } from "eslint";
11+
import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
1212
import * as rule from "../../../rules/prefer-to-have-value";
1313

1414
//------------------------------------------------------------------------------

src/__tests__/rule-tester.js

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/* eslint-disable jest/no-export */
2+
3+
import { RuleTester } from 'eslint';
4+
import semver from 'semver';
5+
import { version as eslintVersion } from 'eslint/package.json';
6+
7+
// we need to have a test as kcd-scripts doesn't let us
8+
// exclude this file from being run via jest as a test
9+
it('is true', () => {
10+
expect(true).toBe(true);
11+
});
12+
13+
export const usingFlatConfig = semver.major(eslintVersion) >= 9;
14+
15+
export class FlatCompatRuleTester extends RuleTester {
16+
constructor(testerConfig) {
17+
super(FlatCompatRuleTester._flatCompat(testerConfig));
18+
}
19+
20+
run(
21+
ruleName,
22+
rule,
23+
tests,
24+
) {
25+
super.run(ruleName, rule, {
26+
valid: tests.valid.map(t => FlatCompatRuleTester._flatCompat(t)),
27+
invalid: tests.invalid.map(t => FlatCompatRuleTester._flatCompat(t)),
28+
});
29+
}
30+
31+
static _flatCompat(config) {
32+
if (!config || !usingFlatConfig || typeof config === 'string') {
33+
return config;
34+
}
35+
36+
const obj = {
37+
languageOptions: { parserOptions: {} },
38+
};
39+
40+
for (const [key, value] of Object.entries(config)) {
41+
if (key === 'parser') {
42+
obj.languageOptions.parser = require(value);
43+
44+
continue;
45+
}
46+
47+
if (key === 'parserOptions') {
48+
for (const [option, val] of Object.entries(value)) {
49+
if (option === 'ecmaVersion' || option === 'sourceType') {
50+
obj.languageOptions[option] = val
51+
52+
continue;
53+
}
54+
55+
obj.languageOptions.parserOptions[option] = val;
56+
}
57+
58+
continue;
59+
}
60+
61+
obj[key] = value;
62+
}
63+
64+
return obj;
65+
}
66+
}

src/assignment-ast.js

+13-10
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
import { queries } from "./queries";
2+
import { getScope } from './context';
23

34
/**
45
* Gets the inner relevant node (CallExpression, Identity, et al.) given a generic expression node
56
* await someAsyncFunc() => someAsyncFunc()
67
* someElement as HTMLDivElement => someElement
78
*
89
* @param {Object} context - Context for a rule
10+
* @param {Object} node - Node for a rule
911
* @param {Object} expression - An expression node
1012
* @returns {Object} - A node
1113
*/
12-
export function getInnerNodeFrom(context, expression) {
14+
export function getInnerNodeFrom(context, node, expression) {
1315
switch (expression.type) {
1416
case "Identifier":
15-
return getAssignmentForIdentifier(context, expression.name);
17+
return getAssignmentForIdentifier(context, node, expression.name);
1618
case "TSAsExpression":
17-
return getInnerNodeFrom(context, expression.expression);
19+
return getInnerNodeFrom(context, node, expression.expression);
1820
case "AwaitExpression":
19-
return getInnerNodeFrom(context, expression.argument);
21+
return getInnerNodeFrom(context, node, expression.argument);
2022
case "MemberExpression":
21-
return getInnerNodeFrom(context, expression.object);
23+
return getInnerNodeFrom(context, node, expression.object);
2224
default:
2325
return expression;
2426
}
@@ -28,19 +30,20 @@ export function getInnerNodeFrom(context, expression) {
2830
* Get the node corresponding to the latest assignment to a variable named `identifierName`
2931
*
3032
* @param {Object} context - Context for a rule
33+
* @param {Object} node - Node for a rule
3134
* @param {String} identifierName - Name of an identifier
3235
* @returns {Object} - A node, possibly undefined
3336
*/
34-
export function getAssignmentForIdentifier(context, identifierName) {
35-
const variable = context.getScope().set.get(identifierName);
37+
export function getAssignmentForIdentifier(context, node, identifierName) {
38+
const variable = getScope(context, node).set.get(identifierName);
3639

3740
if (!variable) return;
3841
const init = variable.defs[0].node.init;
3942

4043
let assignmentNode;
4144
if (init) {
4245
// let foo = bar;
43-
assignmentNode = getInnerNodeFrom(context, init);
46+
assignmentNode = getInnerNodeFrom(context, node, init);
4447
} else {
4548
// let foo;
4649
// foo = bar;
@@ -50,7 +53,7 @@ export function getAssignmentForIdentifier(context, identifierName) {
5053
if (!assignmentRef) {
5154
return;
5255
}
53-
assignmentNode = getInnerNodeFrom(context, assignmentRef.writeExpr);
56+
assignmentNode = getInnerNodeFrom(context, node, assignmentRef.writeExpr);
5457
}
5558
return assignmentNode;
5659
}
@@ -64,7 +67,7 @@ export function getAssignmentForIdentifier(context, identifierName) {
6467
* @returns {Object} - Object with query, queryArg & isDTLQuery
6568
*/
6669
export function getQueryNodeFrom(context, nodeWithValueProp) {
67-
const queryNode = getInnerNodeFrom(context, nodeWithValueProp);
70+
const queryNode = getInnerNodeFrom(context, nodeWithValueProp, nodeWithValueProp);
6871

6972
if (!queryNode || !queryNode.callee) {
7073
return {

src/context.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* istanbul ignore next */
2+
export function getSourceCode(context) {
3+
if ('sourceCode' in context) {
4+
return context.sourceCode;
5+
}
6+
7+
return context.getSourceCode();
8+
}
9+
10+
/* istanbul ignore next */
11+
export function getScope(context, node) {
12+
const sourceCode = getSourceCode(context);
13+
14+
if (sourceCode && sourceCode.getScope) {
15+
return sourceCode.getScope(node);
16+
}
17+
18+
return context.getScope();
19+
}

src/rules/prefer-empty.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @fileoverview Prefer toBeEmpty over checking innerHTML
33
* @author Ben Monro
44
*/
5+
import { getSourceCode } from '../context';
56

67
export const meta = {
78
docs: {
@@ -16,7 +17,7 @@ export const meta = {
1617
export const create = (context) => {
1718
function isNonEmptyStringOrTemplateLiteral(node) {
1819
return !['""', "''", "``", "null"].includes(
19-
context.getSourceCode().getText(node)
20+
getSourceCode(context).getText(node)
2021
);
2122
}
2223

src/rules/prefer-in-document.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { queries } from "../queries";
99
import { getAssignmentForIdentifier } from "../assignment-ast";
10+
import { getSourceCode } from '../context';
1011

1112
export const meta = {
1213
type: "suggestion",
@@ -78,6 +79,7 @@ export const create = (context) => {
7879
if (matcherArguments[0].type === "Identifier") {
7980
const assignment = getAssignmentForIdentifier(
8081
context,
82+
matcherArguments[0],
8183
matcherArguments[0].name
8284
);
8385
if (!assignment) {
@@ -186,7 +188,7 @@ export const create = (context) => {
186188

187189
// Remove any arguments in the matcher
188190
for (const argument of Array.from(matcherArguments)) {
189-
const sourceCode = context.getSourceCode();
191+
const sourceCode = getSourceCode(context);
190192
const token = sourceCode.getTokenAfter(argument);
191193
if (token.value === "," && token.type === "Punctuator") {
192194
// Remove commas if toHaveLength had more than one argument or a trailing comma
@@ -257,6 +259,7 @@ export const create = (context) => {
257259
) {
258260
const queryNode = getAssignmentForIdentifier(
259261
context,
262+
node,
260263
node.object.object.arguments[0].name
261264
);
262265

@@ -285,6 +288,7 @@ export const create = (context) => {
285288
// Value expression being assigned to the left-hand value
286289
const rightValueNode = getAssignmentForIdentifier(
287290
context,
291+
node,
288292
node.object.arguments[0].name
289293
);
290294

src/rules/prefer-to-have-attribute.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @fileoverview prefer toHaveAttribute over checking getAttribute/hasAttribute
33
* @author Ben Monro
44
*/
5+
import { getSourceCode } from '../context';
56

67
//------------------------------------------------------------------------------
78
// Rule Definition
@@ -42,7 +43,7 @@ export const create = (context) => ({
4243
[`CallExpression[callee.property.name='getAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toContain$|toMatch$/]`](
4344
node
4445
) {
45-
const sourceCode = context.getSourceCode();
46+
const sourceCode = getSourceCode(context);
4647
context.report({
4748
node: node.parent,
4849
message: `Use toHaveAttribute instead of asserting on getAttribute`,
@@ -66,7 +67,7 @@ export const create = (context) => ({
6667
const arg = node.parent.parent.parent.arguments;
6768
const isNull = arg.length > 0 && arg[0].value === null;
6869

69-
const sourceCode = context.getSourceCode();
70+
const sourceCode = getSourceCode(context);
7071
context.report({
7172
node: node.parent,
7273
message: `Use toHaveAttribute instead of asserting on getAttribute`,
@@ -127,7 +128,7 @@ export const create = (context) => ({
127128
),
128129
fixer.replaceText(
129130
node.parent.parent.parent.arguments[0],
130-
context.getSourceCode().getText(node.arguments[0])
131+
getSourceCode(context).getText(node.arguments[0])
131132
),
132133
],
133134
});

0 commit comments

Comments
 (0)