Skip to content

feat: support ESLint v9 #355

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 19, 2024
12 changes: 11 additions & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ jobs:
strategy:
fail-fast: false
matrix:
eslint: [6, 7, 8]
eslint: [6, 7, 8, 9]
node: [12.x, 14.x, 16.x, 18.x, 20.x, 21.x]
testing-library-dom: [8, 9, 10]
exclude:
- eslint: 9
node: 12.x
- eslint: 9
node: 14.x
- eslint: 9
node: 16.x
- testing-library-dom: 9
node: 12.x
- testing-library-dom: 10
Expand All @@ -49,6 +55,10 @@ jobs:
with:
useLockFile: false

# see https://github.com/npm/cli/issues/7349
- if: ${{ matrix.eslint == 9 }}
run: npm un @typescript-eslint/parser

- name: Install ESLint v${{ matrix.eslint }}
run: npm install --no-save --force eslint@${{ matrix.eslint }}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,12 @@
"eslint-remote-tester": "^3.0.0",
"eslint-remote-tester-repositories": "^1.0.1",
"kcd-scripts": "^12.0.0",
"semver": "^7.6.0",
"typescript": "^5.1.3"
},
"peerDependencies": {
"@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0",
"eslint": "^6.8.0 || ^7.0.0 || ^8.0.0"
"eslint": "^6.8.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
},
"peerDependenciesMeta": {
"@testing-library/dom": {
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/lib/rules/prefer-empty.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
// Requirements
//------------------------------------------------------------------------------

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

//------------------------------------------------------------------------------
// Tests
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/lib/rules/prefer-focus.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @author Ben Monro
*/

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

const ruleTester = new RuleTester();
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/lib/rules/prefer-in-document.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// Requirements
//------------------------------------------------------------------------------

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

//------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/lib/rules/prefer-prefer-to-have-class.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RuleTester } from "eslint";
import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
import * as rule from "../../../rules/prefer-to-have-class";

const errors = [{ messageId: "use-to-have-class" }];
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/lib/rules/prefer-to-have-attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// Requirements
//------------------------------------------------------------------------------

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

//------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/lib/rules/prefer-to-have-style.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RuleTester } from "eslint";
import { FlatCompatRuleTester as RuleTester } from '../../rule-tester';
import * as rule from "../../../rules/prefer-to-have-style";

const errors = [
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/lib/rules/prefer-to-have-text-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// Requirements
//------------------------------------------------------------------------------

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

//------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/lib/rules/prefer-to-have-value.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// Requirements
//------------------------------------------------------------------------------

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

//------------------------------------------------------------------------------
Expand Down
66 changes: 66 additions & 0 deletions src/__tests__/rule-tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* eslint-disable jest/no-export */

import { RuleTester } from 'eslint';
import semver from 'semver';
import { version as eslintVersion } from 'eslint/package.json';

// we need to have a test as kcd-scripts doesn't let us
// exclude this file from being run via jest as a test
it('is true', () => {
expect(true).toBe(true);
});

export const usingFlatConfig = semver.major(eslintVersion) >= 9;

export class FlatCompatRuleTester extends RuleTester {
constructor(testerConfig) {
super(FlatCompatRuleTester._flatCompat(testerConfig));
}

run(
ruleName,
rule,
tests,
) {
super.run(ruleName, rule, {
valid: tests.valid.map(t => FlatCompatRuleTester._flatCompat(t)),
invalid: tests.invalid.map(t => FlatCompatRuleTester._flatCompat(t)),
});
}

static _flatCompat(config) {
if (!config || !usingFlatConfig || typeof config === 'string') {
return config;
}

const obj = {
languageOptions: { parserOptions: {} },
};

for (const [key, value] of Object.entries(config)) {
if (key === 'parser') {
obj.languageOptions.parser = require(value);

continue;
}

if (key === 'parserOptions') {
for (const [option, val] of Object.entries(value)) {
if (option === 'ecmaVersion' || option === 'sourceType') {
obj.languageOptions[option] = val

continue;
}

obj.languageOptions.parserOptions[option] = val;
}

continue;
}

obj[key] = value;
}

return obj;
}
}
23 changes: 13 additions & 10 deletions src/assignment-ast.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import { queries } from "./queries";
import { getScope } from './context';

/**
* Gets the inner relevant node (CallExpression, Identity, et al.) given a generic expression node
* await someAsyncFunc() => someAsyncFunc()
* someElement as HTMLDivElement => someElement
*
* @param {Object} context - Context for a rule
* @param {Object} node - Node for a rule
* @param {Object} expression - An expression node
* @returns {Object} - A node
*/
export function getInnerNodeFrom(context, expression) {
export function getInnerNodeFrom(context, node, expression) {
switch (expression.type) {
case "Identifier":
return getAssignmentForIdentifier(context, expression.name);
return getAssignmentForIdentifier(context, node, expression.name);
case "TSAsExpression":
return getInnerNodeFrom(context, expression.expression);
return getInnerNodeFrom(context, node, expression.expression);
case "AwaitExpression":
return getInnerNodeFrom(context, expression.argument);
return getInnerNodeFrom(context, node, expression.argument);
case "MemberExpression":
return getInnerNodeFrom(context, expression.object);
return getInnerNodeFrom(context, node, expression.object);
default:
return expression;
}
Expand All @@ -28,19 +30,20 @@ export function getInnerNodeFrom(context, expression) {
* Get the node corresponding to the latest assignment to a variable named `identifierName`
*
* @param {Object} context - Context for a rule
* @param {Object} node - Node for a rule
* @param {String} identifierName - Name of an identifier
* @returns {Object} - A node, possibly undefined
*/
export function getAssignmentForIdentifier(context, identifierName) {
const variable = context.getScope().set.get(identifierName);
export function getAssignmentForIdentifier(context, node, identifierName) {
const variable = getScope(context, node).set.get(identifierName);

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

let assignmentNode;
if (init) {
// let foo = bar;
assignmentNode = getInnerNodeFrom(context, init);
assignmentNode = getInnerNodeFrom(context, node, init);
} else {
// let foo;
// foo = bar;
Expand All @@ -50,7 +53,7 @@ export function getAssignmentForIdentifier(context, identifierName) {
if (!assignmentRef) {
return;
}
assignmentNode = getInnerNodeFrom(context, assignmentRef.writeExpr);
assignmentNode = getInnerNodeFrom(context, node, assignmentRef.writeExpr);
}
return assignmentNode;
}
Expand All @@ -64,7 +67,7 @@ export function getAssignmentForIdentifier(context, identifierName) {
* @returns {Object} - Object with query, queryArg & isDTLQuery
*/
export function getQueryNodeFrom(context, nodeWithValueProp) {
const queryNode = getInnerNodeFrom(context, nodeWithValueProp);
const queryNode = getInnerNodeFrom(context, nodeWithValueProp, nodeWithValueProp);

if (!queryNode || !queryNode.callee) {
return {
Expand Down
19 changes: 19 additions & 0 deletions src/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* istanbul ignore next */
export function getSourceCode(context) {
if ('sourceCode' in context) {
return context.sourceCode;
}

return context.getSourceCode();
}

/* istanbul ignore next */
export function getScope(context, node) {
const sourceCode = getSourceCode(context);

if (sourceCode && sourceCode.getScope) {
return sourceCode.getScope(node);
}

return context.getScope();
}
3 changes: 2 additions & 1 deletion src/rules/prefer-empty.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @fileoverview Prefer toBeEmpty over checking innerHTML
* @author Ben Monro
*/
import { getSourceCode } from '../context';

export const meta = {
docs: {
Expand All @@ -16,7 +17,7 @@ export const meta = {
export const create = (context) => {
function isNonEmptyStringOrTemplateLiteral(node) {
return !['""', "''", "``", "null"].includes(
context.getSourceCode().getText(node)
getSourceCode(context).getText(node)
);
}

Expand Down
6 changes: 5 additions & 1 deletion src/rules/prefer-in-document.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { queries } from "../queries";
import { getAssignmentForIdentifier } from "../assignment-ast";
import { getSourceCode } from '../context';

export const meta = {
type: "suggestion",
Expand Down Expand Up @@ -78,6 +79,7 @@ export const create = (context) => {
if (matcherArguments[0].type === "Identifier") {
const assignment = getAssignmentForIdentifier(
context,
matcherArguments[0],
matcherArguments[0].name
);
if (!assignment) {
Expand Down Expand Up @@ -186,7 +188,7 @@ export const create = (context) => {

// Remove any arguments in the matcher
for (const argument of Array.from(matcherArguments)) {
const sourceCode = context.getSourceCode();
const sourceCode = getSourceCode(context);
const token = sourceCode.getTokenAfter(argument);
if (token.value === "," && token.type === "Punctuator") {
// Remove commas if toHaveLength had more than one argument or a trailing comma
Expand Down Expand Up @@ -257,6 +259,7 @@ export const create = (context) => {
) {
const queryNode = getAssignmentForIdentifier(
context,
node,
node.object.object.arguments[0].name
);

Expand Down Expand Up @@ -285,6 +288,7 @@ export const create = (context) => {
// Value expression being assigned to the left-hand value
const rightValueNode = getAssignmentForIdentifier(
context,
node,
node.object.arguments[0].name
);

Expand Down
7 changes: 4 additions & 3 deletions src/rules/prefer-to-have-attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @fileoverview prefer toHaveAttribute over checking getAttribute/hasAttribute
* @author Ben Monro
*/
import { getSourceCode } from '../context';

//------------------------------------------------------------------------------
// Rule Definition
Expand Down Expand Up @@ -42,7 +43,7 @@ export const create = (context) => ({
[`CallExpression[callee.property.name='getAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toContain$|toMatch$/]`](
node
) {
const sourceCode = context.getSourceCode();
const sourceCode = getSourceCode(context);
context.report({
node: node.parent,
message: `Use toHaveAttribute instead of asserting on getAttribute`,
Expand All @@ -66,7 +67,7 @@ export const create = (context) => ({
const arg = node.parent.parent.parent.arguments;
const isNull = arg.length > 0 && arg[0].value === null;

const sourceCode = context.getSourceCode();
const sourceCode = getSourceCode(context);
context.report({
node: node.parent,
message: `Use toHaveAttribute instead of asserting on getAttribute`,
Expand Down Expand Up @@ -127,7 +128,7 @@ export const create = (context) => ({
),
fixer.replaceText(
node.parent.parent.parent.arguments[0],
context.getSourceCode().getText(node.arguments[0])
getSourceCode(context).getText(node.arguments[0])
),
],
});
Expand Down
Loading
Loading