Skip to content

Commit

Permalink
feat: support ESLint v9 (#355)
Browse files Browse the repository at this point in the history
  • Loading branch information
G-Rath authored Apr 19, 2024
1 parent de56a19 commit 3965c8f
Show file tree
Hide file tree
Showing 20 changed files with 154 additions and 45 deletions.
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

0 comments on commit 3965c8f

Please sign in to comment.