Skip to content
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

feat: support ESLint v9 #185

Merged
merged 4 commits into from
Apr 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ module.exports = {
},
},
{
files: ['src/**/*', 'dangerfile.ts'],
files: ['src/**/*', 'dangerfile.ts', './jest.config.ts'],
parserOptions: {
sourceType: 'module',
},
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,14 @@ jobs:
fail-fast: false
matrix:
node-version: [14.x, 16.x, 18.x, 19.x, 20.x, 21.x]
eslint-version: [7, 8]
eslint-version: [7, 8, 9]
exclude:
# eslint@9 doesn't support node@16
- node-version: 16.x
eslint-version: 9
# eslint@9 doesn't support node@14
- node-version: 14.x
eslint-version: 9
runs-on: ubuntu-latest

steps:
Expand All @@ -92,11 +99,11 @@ jobs:
yarn add --dev eslint@${{ matrix.eslint-version }}
- name: run tests
# only collect coverage on eslint versions that support dynamic import
run: yarn test --coverage ${{ matrix.eslint-version >= 8 }}
run: yarn test --coverage ${{ matrix.eslint-version == 8 }}
env:
CI: true
- uses: codecov/codecov-action@v3
if: ${{ matrix.eslint-version >= 8 }}
if: ${{ matrix.eslint-version == 8 }}
test-ubuntu:
uses: ./.github/workflows/test.yml
needs: prepare-yarn-cache-ubuntu
Expand Down
44 changes: 44 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { version as eslintVersion } from 'eslint/package.json';
import type { Config } from 'jest';
import * as semver from 'semver';

const config = {
clearMocks: true,
restoreMocks: true,
resetMocks: true,

coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
},
},

projects: [
{
displayName: 'test',
testPathIgnorePatterns: [
'<rootDir>/lib/.*',
'<rootDir>/src/rules/__tests__/test-utils.ts',
],
coveragePathIgnorePatterns: ['/node_modules/'],
},
{
displayName: 'lint',
runner: 'jest-runner-eslint',
testMatch: ['<rootDir>/**/*.{js,ts}'],
testPathIgnorePatterns: ['<rootDir>/lib/.*'],
coveragePathIgnorePatterns: ['/node_modules/'],
},
],
} satisfies Config;

if (semver.major(eslintVersion) >= 9) {
config.projects = config.projects.filter(
({ displayName }) => displayName !== 'lint',
);
}

export default config;
35 changes: 3 additions & 32 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,35 +69,6 @@
"@semantic-release/github"
]
},
"jest": {
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
},
"projects": [
{
"displayName": "test",
"testPathIgnorePatterns": [
"<rootDir>/lib/.*",
"<rootDir>/src/rules/__tests__/test-utils.ts"
]
},
{
"displayName": "lint",
"runner": "jest-runner-eslint",
"testMatch": [
"<rootDir>/**/*.{js,ts}"
],
"testPathIgnorePatterns": [
"<rootDir>/lib/.*"
]
}
]
},
"resolutions": {
"@typescript-eslint/experimental-utils": "^5.0.0"
},
Expand All @@ -123,7 +94,7 @@
"babel-jest": "^29.0.0",
"babel-plugin-replace-ts-export-assignment": "^0.0.2",
"dedent": "^1.0.0",
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0",
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-doc-generator": "^1.0.0",
"eslint-plugin-eslint-comments": "^3.1.2",
Expand All @@ -143,12 +114,12 @@
"prettier": "^3.0.0",
"rimraf": "^5.0.0",
"semantic-release": "^23.0.0",
"semver": "^7.0.0",
"semver": "^7.6.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.0"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0"
"eslint": "^7.0.0 || ^8.0.0 || ^9.0.0"
},
"packageManager": "yarn@3.8.1",
"engines": {
Expand Down
6 changes: 3 additions & 3 deletions src/rules/__tests__/prefer-to-be-array.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { TSESLint } from '@typescript-eslint/utils';
import type { TSESLint } from '@typescript-eslint/utils';
import rule, { type MessageIds, type Options } from '../prefer-to-be-array';
import { espreeParser } from './test-utils';
import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils';

const ruleTester = new TSESLint.RuleTester({
const ruleTester = new RuleTester({
parser: espreeParser,
parserOptions: {
ecmaVersion: 2017,
Expand Down
6 changes: 3 additions & 3 deletions src/rules/__tests__/prefer-to-be-false.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TSESLint } from '@typescript-eslint/utils';
import rule from '../prefer-to-be-false';
import { FlatCompatRuleTester as RuleTester } from './test-utils';

const ruleTester = new TSESLint.RuleTester();
const ruleTester = new RuleTester();

ruleTester.run('prefer-to-be-false', rule, {
valid: [
Expand Down Expand Up @@ -58,7 +58,7 @@ ruleTester.run('prefer-to-be-false', rule, {
],
});

new TSESLint.RuleTester({
new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
}).run('prefer-to-be-false: typescript edition', rule, {
valid: [
Expand Down
5 changes: 3 additions & 2 deletions src/rules/__tests__/prefer-to-be-object.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { TSESLint } from '@typescript-eslint/utils';
import type { TSESLint } from '@typescript-eslint/utils';
import rule, { type MessageIds, type Options } from '../prefer-to-be-object';
import { FlatCompatRuleTester as RuleTester } from './test-utils';

const ruleTester = new TSESLint.RuleTester();
const ruleTester = new RuleTester();

// makes ts happy about the dynamic test generation
const messageId = 'preferToBeObject' as const;
Expand Down
6 changes: 3 additions & 3 deletions src/rules/__tests__/prefer-to-be-true.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TSESLint } from '@typescript-eslint/utils';
import rule from '../prefer-to-be-true';
import { FlatCompatRuleTester as RuleTester } from './test-utils';

const ruleTester = new TSESLint.RuleTester();
const ruleTester = new RuleTester();

ruleTester.run('prefer-to-be-true', rule, {
valid: [
Expand Down Expand Up @@ -58,7 +58,7 @@ ruleTester.run('prefer-to-be-true', rule, {
],
});

new TSESLint.RuleTester({
new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
}).run('prefer-to-be-true: typescript edition', rule, {
valid: [
Expand Down
6 changes: 3 additions & 3 deletions src/rules/__tests__/prefer-to-have-been-called-once.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TSESLint } from '@typescript-eslint/utils';
import rule from '../prefer-to-have-been-called-once';
import { FlatCompatRuleTester as RuleTester } from './test-utils';

const ruleTester = new TSESLint.RuleTester();
const ruleTester = new RuleTester();

ruleTester.run('prefer-to-have-been-called-once', rule, {
valid: [
Expand Down Expand Up @@ -38,7 +38,7 @@ ruleTester.run('prefer-to-have-been-called-once', rule, {
],
});

new TSESLint.RuleTester({
new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
}).run('prefer-to-have-been-called-once: typescript edition', rule, {
valid: [
Expand Down
157 changes: 157 additions & 0 deletions src/rules/__tests__/test-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,163 @@
import { createRequire } from 'module';
import { TSESLint } from '@typescript-eslint/utils';
import { version as eslintVersion } from 'eslint/package.json';
import * as semver from 'semver';

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

export const espreeParser = eslintRequire.resolve('espree');

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

export class FlatCompatRuleTester extends TSESLint.RuleTester {
public constructor(testerConfig?: TSESLint.RuleTesterConfig) {
super(FlatCompatRuleTester._flatCompat(testerConfig));
}

public override run<
TMessageIds extends string,
TOptions extends Readonly<unknown[]>,
>(
ruleName: string,
rule: TSESLint.RuleModule<TMessageIds, TOptions>,
tests: TSESLint.RunTests<TMessageIds, TOptions>,
) {
super.run(ruleName, rule, {
valid: tests.valid.map(t => FlatCompatRuleTester._flatCompat(t)),
invalid: tests.invalid.map(t => FlatCompatRuleTester._flatCompat(t)),
});
}

/* istanbul ignore next */
private static _flatCompat<
T extends
| undefined
| RuleTesterConfig
| string
| TSESLint.ValidTestCase<unknown[]>
| TSESLint.InvalidTestCase<string, unknown[]>,
>(config: T): T {
if (!config || !usingFlatConfig || typeof config === 'string') {
return config;
}

const obj: FlatConfig.Config & {
languageOptions: FlatConfig.LanguageOptions & {
parserOptions: FlatConfig.ParserOptions;
};
} = {
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') {
// @ts-expect-error: TS thinks the value could the opposite type of whatever option is
obj.languageOptions[option] = val as FlatConfig.LanguageOptions[
| 'ecmaVersion'
| 'sourceType'];

continue;
}

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

continue;
}

obj[key as keyof typeof obj] = value;
}

return obj as unknown as T;
}
}

type RuleTesterConfig = TSESLint.RuleTesterConfig | FlatConfig.Config;

export declare namespace FlatConfig {
type EcmaVersion = TSESLint.EcmaVersion;
type ParserOptions = TSESLint.ParserOptions;
type SourceType = TSESLint.SourceType | 'commonjs';
interface LanguageOptions {
/**
* The version of ECMAScript to support.
* May be any year (i.e., `2022`) or version (i.e., `5`).
* Set to `"latest"` for the most recent supported version.
* @default "latest"
*/
ecmaVersion?: EcmaVersion;
/**
* An object specifying additional objects that should be added to the global scope during linting.
*/
globals?: unknown;
/**
* An object containing a `parse()` method or a `parseForESLint()` method.
* @default
* ```
* // https://github.com/eslint/espree
* require('espree')
* ```
*/
parser?: unknown;
/**
* An object specifying additional options that are passed directly to the parser.
* The available options are parser-dependent.
*/
parserOptions?: ParserOptions;
/**
* The type of JavaScript source code.
* Possible values are `"script"` for traditional script files, `"module"` for ECMAScript modules (ESM), and `"commonjs"` for CommonJS files.
* @default
* ```
* // for `.js` and `.mjs` files
* "module"
* // for `.cjs` files
* "commonjs"
* ```
*/
sourceType?: SourceType;
}
interface Config {
/**
* An array of glob patterns indicating the files that the configuration object should apply to.
* If not specified, the configuration object applies to all files matched by any other configuration object.
*/
files?: string[];
/**
* An array of glob patterns indicating the files that the configuration object should not apply to.
* If not specified, the configuration object applies to all files matched by files.
*/
ignores?: string[];
/**
* An object containing settings related to how JavaScript is configured for linting.
*/
languageOptions?: LanguageOptions;
/**
* An object containing settings related to the linting process.
*/
linterOptions?: unknown;
/**
* An object containing a name-value mapping of plugin names to plugin objects.
* When `files` is specified, these plugins are only available to the matching files.
*/
plugins?: unknown;
/**
* An object containing the configured rules.
* When `files` or `ignores` are specified, these rule configurations are only available to the matching files.
*/
rules?: unknown;
/**
* An object containing name-value pairs of information that should be available to all rules.
*/
settings?: unknown;
}
}
Loading
Loading