Skip to content

Commit d7a9c81

Browse files
committed
feat: add support for eslint 8
Closes: #664
1 parent 34ebcd8 commit d7a9c81

12 files changed

+14271
-4324
lines changed

src/eslint-reporter/reporter/EsLintReporter.ts

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,36 @@
11
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2-
import { CLIEngine, LintReport, LintResult } from '../types/eslint';
2+
import { CLIEngine, ESLintOrCLIEngine, LintReport, LintResult } from '../types/eslint';
33
import { createIssuesFromEsLintResults } from '../issue/EsLintIssueFactory';
44
import { EsLintReporterConfiguration } from '../EsLintReporterConfiguration';
55
import { Reporter } from '../../reporter';
6-
import { normalize } from 'path';
6+
import path from 'path';
7+
import fs from 'fs-extra';
78
import minimatch from 'minimatch';
89
import glob from 'glob';
910

11+
const isOldCLIEngine = (eslint: ESLintOrCLIEngine): eslint is CLIEngine =>
12+
(eslint as CLIEngine).resolveFileGlobPatterns !== undefined;
13+
1014
function createEsLintReporter(configuration: EsLintReporterConfiguration): Reporter {
1115
// eslint-disable-next-line @typescript-eslint/no-var-requires
12-
const { CLIEngine } = require('eslint');
13-
const engine: CLIEngine = new CLIEngine(configuration.options);
16+
const { CLIEngine, ESLint } = require('eslint');
17+
18+
const eslint: ESLintOrCLIEngine = ESLint
19+
? new ESLint(configuration.options)
20+
: new CLIEngine(configuration.options);
1421

1522
let isInitialRun = true;
1623
let isInitialGetFiles = true;
1724

1825
const lintResults = new Map<string, LintResult>();
19-
const includedGlobPatterns = engine.resolveFileGlobPatterns(configuration.files);
26+
const includedGlobPatterns = resolveFileGlobPatterns(configuration.files);
2027
const includedFiles = new Set<string>();
2128

22-
function isFileIncluded(path: string) {
29+
async function isFileIncluded(path: string): Promise<boolean> {
2330
return (
2431
!path.includes('node_modules') &&
2532
includedGlobPatterns.some((pattern) => minimatch(path, pattern)) &&
26-
!engine.isPathIgnored(path)
33+
!(await eslint.isPathIgnored(path))
2734
);
2835
}
2936

@@ -49,7 +56,7 @@ function createEsLintReporter(configuration: EsLintReporterConfiguration): Repor
4956

5057
for (const resolvedGlob of resolvedGlobs) {
5158
for (const resolvedFile of resolvedGlob) {
52-
if (isFileIncluded(resolvedFile)) {
59+
if (await isFileIncluded(resolvedFile)) {
5360
includedFiles.add(resolvedFile);
5461
}
5562
}
@@ -67,12 +74,43 @@ function createEsLintReporter(configuration: EsLintReporterConfiguration): Repor
6774
return configuration.options.extensions || [];
6875
}
6976

77+
// Copied from the eslint 6 implementation, as it's not available in eslint 8
78+
function resolveFileGlobPatterns(globPatterns: string[]) {
79+
if (configuration.options.globInputPaths === false) {
80+
return globPatterns.filter(Boolean);
81+
}
82+
83+
const extensions = getExtensions().map((ext) => ext.replace(/^\./u, ''));
84+
const dirSuffix = `/**/*.{${extensions.join(',')}}`;
85+
86+
return globPatterns.filter(Boolean).map((globPattern) => {
87+
const resolvedPath = path.resolve(configuration.options.cwd || '', globPattern);
88+
const newPath = directoryExists(resolvedPath)
89+
? globPattern.replace(/[/\\]$/u, '') + dirSuffix
90+
: globPattern;
91+
92+
return path.normalize(newPath).replace(/\\/gu, '/');
93+
});
94+
}
95+
96+
// Copied from the eslint 6 implementation, as it's not available in eslint 8
97+
function directoryExists(resolvedPath: string) {
98+
try {
99+
return fs.statSync(resolvedPath).isDirectory();
100+
} catch (error) {
101+
if (error && error.code === 'ENOENT') {
102+
return false;
103+
}
104+
throw error;
105+
}
106+
}
107+
70108
return {
71109
getReport: async ({ changedFiles = [], deletedFiles = [] }) => {
72110
return {
73111
async getDependencies() {
74112
for (const changedFile of changedFiles) {
75-
if (isFileIncluded(changedFile)) {
113+
if (await isFileIncluded(changedFile)) {
76114
includedFiles.add(changedFile);
77115
}
78116
}
@@ -81,8 +119,8 @@ function createEsLintReporter(configuration: EsLintReporterConfiguration): Repor
81119
}
82120

83121
return {
84-
files: (await getFiles()).map((file) => normalize(file)),
85-
dirs: getDirs().map((dir) => normalize(dir)),
122+
files: (await getFiles()).map((file) => path.normalize(file)),
123+
dirs: getDirs().map((dir) => path.normalize(dir)),
86124
excluded: [],
87125
extensions: getExtensions(),
88126
};
@@ -100,23 +138,38 @@ function createEsLintReporter(configuration: EsLintReporterConfiguration): Repor
100138
const lintReports: LintReport[] = [];
101139

102140
if (isInitialRun) {
103-
lintReports.push(engine.executeOnFiles(includedGlobPatterns));
141+
const lintReport: LintReport = await (isOldCLIEngine(eslint)
142+
? Promise.resolve(eslint.executeOnFiles(includedGlobPatterns))
143+
: eslint.lintFiles(includedGlobPatterns).then((results) => ({ results })));
144+
lintReports.push(lintReport);
104145
isInitialRun = false;
105146
} else {
106147
// we need to take care to not lint files that are not included by the configuration.
107148
// the eslint engine will not exclude them automatically
108-
const changedAndIncludedFiles = changedFiles.filter((changedFile) =>
109-
isFileIncluded(changedFile)
110-
);
149+
const changedAndIncludedFiles: string[] = [];
150+
for (const changedFile of changedFiles) {
151+
if (await isFileIncluded(changedFile)) {
152+
changedAndIncludedFiles.push(changedFile);
153+
}
154+
}
111155

112156
if (changedAndIncludedFiles.length) {
113-
lintReports.push(engine.executeOnFiles(changedAndIncludedFiles));
157+
const lintReport: LintReport = await (isOldCLIEngine(eslint)
158+
? Promise.resolve(eslint.executeOnFiles(changedAndIncludedFiles))
159+
: eslint.lintFiles(changedAndIncludedFiles).then((results) => ({ results })));
160+
lintReports.push(lintReport);
114161
}
115162
}
116163

117164
// output fixes if `fix` option is provided
118165
if (configuration.options.fix) {
119-
await Promise.all(lintReports.map((lintReport) => CLIEngine.outputFixes(lintReport)));
166+
await Promise.all(
167+
lintReports.map((lintReport) =>
168+
isOldCLIEngine(eslint)
169+
? CLIEngine.outputFixes(lintReport)
170+
: ESLint.outputFixes(lintReport.results)
171+
)
172+
);
120173
}
121174

122175
// store results

src/eslint-reporter/types/eslint.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ export interface CLIEngine {
3030
resolveFileGlobPatterns(filesPatterns: string[]): string[];
3131
isPathIgnored(filePath: string): boolean;
3232
}
33+
export interface ESLint {
34+
version: string;
35+
lintFiles(filesPatterns: string[]): Promise<LintResult[]>;
36+
isPathIgnored(filePath: string): Promise<boolean>;
37+
}
38+
39+
export type ESLintOrCLIEngine = CLIEngine | ESLint;
3340

3441
export interface CLIEngineOptions {
3542
cwd?: string;

test/e2e/EsLint.spec.ts

Lines changed: 67 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,18 @@ describe('EsLint', () => {
2424
});
2525

2626
it.each([
27-
{ async: false, webpack: '4.0.0', absolute: false },
28-
{ async: true, webpack: '^4.0.0', absolute: true },
29-
{ async: false, webpack: '^5.0.0', absolute: true },
30-
{ async: true, webpack: '^5.0.0', absolute: false },
31-
])('reports lint error for %p', async ({ async, webpack, absolute }) => {
27+
{ async: false, webpack: '4.0.0', eslint: '^6.0.0', absolute: false },
28+
{ async: true, webpack: '^4.0.0', eslint: '^7.0.0', absolute: true },
29+
{ async: false, webpack: '^5.0.0', eslint: '^7.0.0', absolute: true },
30+
{ async: true, webpack: '^5.0.0', eslint: '^8.0.0', absolute: false },
31+
])('reports lint error for %p', async ({ async, webpack, eslint, absolute }) => {
3232
await sandbox.load([
3333
await readFixture(join(__dirname, 'fixtures/environment/eslint-basic.fixture'), {
3434
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION: JSON.stringify(
3535
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION
3636
),
3737
TS_LOADER_VERSION: JSON.stringify('^5.0.0'),
38+
ESLINT_VERSION: JSON.stringify(eslint),
3839
TYPESCRIPT_VERSION: JSON.stringify('~3.8.0'),
3940
WEBPACK_VERSION: JSON.stringify(webpack),
4041
WEBPACK_CLI_VERSION: JSON.stringify(WEBPACK_CLI_VERSION),
@@ -61,7 +62,7 @@ describe('EsLint', () => {
6162
'WARNING in src/authenticate.ts:14:34',
6263
'@typescript-eslint/no-explicit-any: Unexpected any. Specify a different type.',
6364
' 12 | }',
64-
' 13 | ',
65+
' 13 |',
6566
' > 14 | async function logout(): Promise<any> {',
6667
' | ^^^',
6768
' 15 | const response = await fetch(',
@@ -76,7 +77,7 @@ describe('EsLint', () => {
7677
" > 31 | loginForm.addEventListener('submit', async event => {",
7778
' | ^^^^^',
7879
' 32 | const user = await login(email, password);',
79-
' 33 | ',
80+
' 33 |',
8081
" 34 | if (user.role === 'admin') {",
8182
].join('\n'),
8283
]);
@@ -127,22 +128,22 @@ describe('EsLint', () => {
127128
'WARNING in src/model/User.ts:11:5',
128129
"@typescript-eslint/no-unused-vars: 'temporary' is defined but never used.",
129130
' 9 | }',
130-
' 10 | ',
131+
' 10 |',
131132
' > 11 | let temporary: any;',
132133
' | ^^^^^^^^^^^^^^',
133-
' 12 | ',
134-
' 13 | ',
134+
' 12 |',
135+
' 13 |',
135136
' 14 | function getUserName(user: User): string {',
136137
].join('\n'),
137138
[
138139
'WARNING in src/model/User.ts:11:16',
139140
'@typescript-eslint/no-explicit-any: Unexpected any. Specify a different type.',
140141
' 9 | }',
141-
' 10 | ',
142+
' 10 |',
142143
' > 11 | let temporary: any;',
143144
' | ^^^',
144-
' 12 | ',
145-
' 13 | ',
145+
' 12 |',
146+
' 13 |',
146147
' 14 | function getUserName(user: User): string {',
147148
].join('\n'),
148149
]);
@@ -155,6 +156,7 @@ describe('EsLint', () => {
155156
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION
156157
),
157158
TS_LOADER_VERSION: JSON.stringify('^5.0.0'),
159+
ESLINT_VERSION: JSON.stringify('~6.8.0'),
158160
TYPESCRIPT_VERSION: JSON.stringify('~3.8.0'),
159161
WEBPACK_VERSION: JSON.stringify('^4.0.0'),
160162
WEBPACK_CLI_VERSION: JSON.stringify(WEBPACK_CLI_VERSION),
@@ -210,54 +212,61 @@ describe('EsLint', () => {
210212
await driver.waitForNoErrors();
211213
});
212214

213-
it('fixes errors with `fix: true` option', async () => {
214-
await sandbox.load([
215-
await readFixture(join(__dirname, 'fixtures/environment/eslint-basic.fixture'), {
216-
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION: JSON.stringify(
217-
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION
218-
),
219-
TS_LOADER_VERSION: JSON.stringify('^5.0.0'),
220-
TYPESCRIPT_VERSION: JSON.stringify('~3.8.0'),
221-
WEBPACK_VERSION: JSON.stringify('^4.0.0'),
222-
WEBPACK_CLI_VERSION: JSON.stringify(WEBPACK_CLI_VERSION),
223-
WEBPACK_DEV_SERVER_VERSION: JSON.stringify(WEBPACK_DEV_SERVER_VERSION),
224-
ASYNC: JSON.stringify(false),
225-
}),
226-
await readFixture(join(__dirname, 'fixtures/implementation/typescript-basic.fixture')),
227-
]);
228-
229-
// fix initial issues
230-
await sandbox.patch(
231-
'src/authenticate.ts',
232-
'async function logout(): Promise<any> {',
233-
'async function logout(): Promise<unknown> {'
234-
);
235-
await sandbox.patch(
236-
'src/index.ts',
237-
"loginForm.addEventListener('submit', async event => {",
238-
"loginForm.addEventListener('submit', async () => {"
239-
);
215+
it.each([{ eslint: '^6.0.0' }, { eslint: '^7.0.0' }, { eslint: '^8.0.0' }])(
216+
'fixes errors with `fix: true` option for %p',
217+
async ({ eslint }) => {
218+
await sandbox.load([
219+
await readFixture(join(__dirname, 'fixtures/environment/eslint-basic.fixture'), {
220+
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION: JSON.stringify(
221+
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION
222+
),
223+
TS_LOADER_VERSION: JSON.stringify('^5.0.0'),
224+
ESLINT_VERSION: JSON.stringify(eslint),
225+
TYPESCRIPT_VERSION: JSON.stringify('~3.8.0'),
226+
WEBPACK_VERSION: JSON.stringify('^4.0.0'),
227+
WEBPACK_CLI_VERSION: JSON.stringify(WEBPACK_CLI_VERSION),
228+
WEBPACK_DEV_SERVER_VERSION: JSON.stringify(WEBPACK_DEV_SERVER_VERSION),
229+
ASYNC: JSON.stringify(false),
230+
}),
231+
await readFixture(join(__dirname, 'fixtures/implementation/typescript-basic.fixture')),
232+
]);
233+
234+
// fix initial issues
235+
await sandbox.patch(
236+
'src/authenticate.ts',
237+
'async function logout(): Promise<any> {',
238+
'async function logout(): Promise<unknown> {'
239+
);
240+
await sandbox.patch(
241+
'src/index.ts',
242+
"loginForm.addEventListener('submit', async event => {",
243+
"loginForm.addEventListener('submit', async () => {"
244+
);
240245

241-
// set fix option for the eslint
242-
await sandbox.write(
243-
'fork-ts-checker.config.js',
244-
'module.exports = { eslint: { enabled: true, options: { fix: true } } };'
245-
);
246+
// set fix option for the eslint
247+
await sandbox.write(
248+
'fork-ts-checker.config.js',
249+
'module.exports = { eslint: { enabled: true, options: { fix: true } } };'
250+
);
246251

247-
// add fixable issue
248-
await sandbox.patch(
249-
'src/authenticate.ts',
250-
'const response = await fetch(',
251-
'let response = await fetch('
252-
);
252+
// add fixable issue
253+
await sandbox.patch(
254+
'src/authenticate.ts',
255+
'const response = await fetch(',
256+
'let response = await fetch('
257+
);
253258

254-
const driver = createWebpackDevServerDriver(sandbox.spawn('npm run webpack-dev-server'), false);
259+
const driver = createWebpackDevServerDriver(
260+
sandbox.spawn('npm run webpack-dev-server'),
261+
false
262+
);
255263

256-
// it should be automatically fixed
257-
await driver.waitForNoErrors();
264+
// it should be automatically fixed
265+
await driver.waitForNoErrors();
258266

259-
// check if issue has been fixed
260-
const content = await sandbox.read('src/authenticate.ts');
261-
expect(content).not.toContain('let response = await fetch(');
262-
});
267+
// check if issue has been fixed
268+
const content = await sandbox.read('src/authenticate.ts');
269+
expect(content).not.toContain('let response = await fetch(');
270+
}
271+
);
263272
});

test/e2e/fixtures/environment/eslint-basic.fixture

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
},
1010
"devDependencies": {
1111
"@types/eslint": "^6.8.0",
12-
"@typescript-eslint/eslint-plugin": "^2.27.0",
13-
"@typescript-eslint/parser": "^2.27.0",
12+
"@typescript-eslint/eslint-plugin": "^5.0.0",
13+
"@typescript-eslint/parser": "^5.0.0",
1414
"css-loader": "^3.5.0",
15-
"eslint": "^6.8.0",
15+
"eslint": ${ESLINT_VERSION},
1616
"fork-ts-checker-webpack-plugin": ${FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION},
1717
"style-loader": "^1.2.0",
1818
"ts-loader": ${TS_LOADER_VERSION},
@@ -44,11 +44,11 @@
4444
/// .eslintrc.js
4545
module.exports = {
4646
parser: '@typescript-eslint/parser',
47-
parserOptions: {
48-
ecmaVersion: 2018,
49-
sourceType: 'module'
50-
},
51-
extends: ['plugin:@typescript-eslint/recommended']
47+
plugins: ["@typescript-eslint"],
48+
extends: ["plugin:@typescript-eslint/recommended"],
49+
rules: {
50+
'@typescript-eslint/no-loss-of-precision': 'off'
51+
}
5252
};
5353

5454
/// webpack.config.js

0 commit comments

Comments
 (0)