Skip to content

Commit

Permalink
feat: add support for eslint 8 (#666)
Browse files Browse the repository at this point in the history
Closes: #664
  • Loading branch information
piotr-oles authored Oct 20, 2021
1 parent e9a4e8d commit 551f2fb
Show file tree
Hide file tree
Showing 13 changed files with 14,292 additions and 4,325 deletions.
87 changes: 70 additions & 17 deletions src/eslint-reporter/reporter/EsLintReporter.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { CLIEngine, LintReport, LintResult } from '../types/eslint';
import { CLIEngine, ESLintOrCLIEngine, LintReport, LintResult } from '../types/eslint';
import { createIssuesFromEsLintResults } from '../issue/EsLintIssueFactory';
import { EsLintReporterConfiguration } from '../EsLintReporterConfiguration';
import { Reporter } from '../../reporter';
import { normalize } from 'path';
import path from 'path';
import fs from 'fs-extra';
import minimatch from 'minimatch';
import glob from 'glob';

const isOldCLIEngine = (eslint: ESLintOrCLIEngine): eslint is CLIEngine =>
(eslint as CLIEngine).resolveFileGlobPatterns !== undefined;

function createEsLintReporter(configuration: EsLintReporterConfiguration): Reporter {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { CLIEngine } = require('eslint');
const engine: CLIEngine = new CLIEngine(configuration.options);
const { CLIEngine, ESLint } = require('eslint');

const eslint: ESLintOrCLIEngine = ESLint
? new ESLint(configuration.options)
: new CLIEngine(configuration.options);

let isInitialRun = true;
let isInitialGetFiles = true;

const lintResults = new Map<string, LintResult>();
const includedGlobPatterns = engine.resolveFileGlobPatterns(configuration.files);
const includedGlobPatterns = resolveFileGlobPatterns(configuration.files);
const includedFiles = new Set<string>();

function isFileIncluded(path: string) {
async function isFileIncluded(path: string): Promise<boolean> {
return (
!path.includes('node_modules') &&
includedGlobPatterns.some((pattern) => minimatch(path, pattern)) &&
!engine.isPathIgnored(path)
!(await eslint.isPathIgnored(path))
);
}

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

for (const resolvedGlob of resolvedGlobs) {
for (const resolvedFile of resolvedGlob) {
if (isFileIncluded(resolvedFile)) {
if (await isFileIncluded(resolvedFile)) {
includedFiles.add(resolvedFile);
}
}
Expand All @@ -67,12 +74,43 @@ function createEsLintReporter(configuration: EsLintReporterConfiguration): Repor
return configuration.options.extensions || [];
}

// Copied from the eslint 6 implementation, as it's not available in eslint 8
function resolveFileGlobPatterns(globPatterns: string[]) {
if (configuration.options.globInputPaths === false) {
return globPatterns.filter(Boolean);
}

const extensions = getExtensions().map((ext) => ext.replace(/^\./u, ''));
const dirSuffix = `/**/*.{${extensions.join(',')}}`;

return globPatterns.filter(Boolean).map((globPattern) => {
const resolvedPath = path.resolve(configuration.options.cwd || '', globPattern);
const newPath = directoryExists(resolvedPath)
? globPattern.replace(/[/\\]$/u, '') + dirSuffix
: globPattern;

return path.normalize(newPath).replace(/\\/gu, '/');
});
}

// Copied from the eslint 6 implementation, as it's not available in eslint 8
function directoryExists(resolvedPath: string) {
try {
return fs.statSync(resolvedPath).isDirectory();
} catch (error) {
if (error && error.code === 'ENOENT') {
return false;
}
throw error;
}
}

return {
getReport: async ({ changedFiles = [], deletedFiles = [] }) => {
return {
async getDependencies() {
for (const changedFile of changedFiles) {
if (isFileIncluded(changedFile)) {
if (await isFileIncluded(changedFile)) {
includedFiles.add(changedFile);
}
}
Expand All @@ -81,8 +119,8 @@ function createEsLintReporter(configuration: EsLintReporterConfiguration): Repor
}

return {
files: (await getFiles()).map((file) => normalize(file)),
dirs: getDirs().map((dir) => normalize(dir)),
files: (await getFiles()).map((file) => path.normalize(file)),
dirs: getDirs().map((dir) => path.normalize(dir)),
excluded: [],
extensions: getExtensions(),
};
Expand All @@ -100,23 +138,38 @@ function createEsLintReporter(configuration: EsLintReporterConfiguration): Repor
const lintReports: LintReport[] = [];

if (isInitialRun) {
lintReports.push(engine.executeOnFiles(includedGlobPatterns));
const lintReport: LintReport = await (isOldCLIEngine(eslint)
? Promise.resolve(eslint.executeOnFiles(includedGlobPatterns))
: eslint.lintFiles(includedGlobPatterns).then((results) => ({ results })));
lintReports.push(lintReport);
isInitialRun = false;
} else {
// we need to take care to not lint files that are not included by the configuration.
// the eslint engine will not exclude them automatically
const changedAndIncludedFiles = changedFiles.filter((changedFile) =>
isFileIncluded(changedFile)
);
const changedAndIncludedFiles: string[] = [];
for (const changedFile of changedFiles) {
if (await isFileIncluded(changedFile)) {
changedAndIncludedFiles.push(changedFile);
}
}

if (changedAndIncludedFiles.length) {
lintReports.push(engine.executeOnFiles(changedAndIncludedFiles));
const lintReport: LintReport = await (isOldCLIEngine(eslint)
? Promise.resolve(eslint.executeOnFiles(changedAndIncludedFiles))
: eslint.lintFiles(changedAndIncludedFiles).then((results) => ({ results })));
lintReports.push(lintReport);
}
}

// output fixes if `fix` option is provided
if (configuration.options.fix) {
await Promise.all(lintReports.map((lintReport) => CLIEngine.outputFixes(lintReport)));
await Promise.all(
lintReports.map((lintReport) =>
isOldCLIEngine(eslint)
? CLIEngine.outputFixes(lintReport)
: ESLint.outputFixes(lintReport.results)
)
);
}

// store results
Expand Down
7 changes: 7 additions & 0 deletions src/eslint-reporter/types/eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export interface CLIEngine {
resolveFileGlobPatterns(filesPatterns: string[]): string[];
isPathIgnored(filePath: string): boolean;
}
export interface ESLint {
version: string;
lintFiles(filesPatterns: string[]): Promise<LintResult[]>;
isPathIgnored(filePath: string): Promise<boolean>;
}

export type ESLintOrCLIEngine = CLIEngine | ESLint;

export interface CLIEngineOptions {
cwd?: string;
Expand Down
145 changes: 87 additions & 58 deletions test/e2e/EsLint.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { join } from 'path';
import process from 'process';
import { readFixture } from './sandbox/Fixture';
import { Sandbox, createSandbox } from './sandbox/Sandbox';
import {
Expand All @@ -8,6 +9,8 @@ import {
} from './sandbox/WebpackDevServerDriver';
import { FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION } from './sandbox/Plugin';

const ignored = process.version.startsWith('v10');

describe('EsLint', () => {
let sandbox: Sandbox;

Expand All @@ -24,17 +27,27 @@ describe('EsLint', () => {
});

it.each([
{ async: false, webpack: '4.0.0', absolute: false },
{ async: true, webpack: '^4.0.0', absolute: true },
{ async: false, webpack: '^5.0.0', absolute: true },
{ async: true, webpack: '^5.0.0', absolute: false },
])('reports lint error for %p', async ({ async, webpack, absolute }) => {
{ async: false, webpack: '4.0.0', eslint: '^6.0.0', absolute: false },
{ async: true, webpack: '^4.0.0', eslint: '^7.0.0', absolute: true },
{ async: false, webpack: '^5.0.0', eslint: '^7.0.0', absolute: true },
{
async: true,
webpack: '^5.0.0',
eslint: '^8.0.0',
absolute: false,
},
])('reports lint error for %p', async ({ async, webpack, eslint, absolute }) => {
if (ignored) {
console.warn('Ignoring test - incompatible node version');
return;
}
await sandbox.load([
await readFixture(join(__dirname, 'fixtures/environment/eslint-basic.fixture'), {
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION: JSON.stringify(
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION
),
TS_LOADER_VERSION: JSON.stringify('^5.0.0'),
ESLINT_VERSION: JSON.stringify(eslint),
TYPESCRIPT_VERSION: JSON.stringify('~3.8.0'),
WEBPACK_VERSION: JSON.stringify(webpack),
WEBPACK_CLI_VERSION: JSON.stringify(WEBPACK_CLI_VERSION),
Expand All @@ -61,7 +74,7 @@ describe('EsLint', () => {
'WARNING in src/authenticate.ts:14:34',
'@typescript-eslint/no-explicit-any: Unexpected any. Specify a different type.',
' 12 | }',
' 13 | ',
' 13 |',
' > 14 | async function logout(): Promise<any> {',
' | ^^^',
' 15 | const response = await fetch(',
Expand All @@ -76,7 +89,7 @@ describe('EsLint', () => {
" > 31 | loginForm.addEventListener('submit', async event => {",
' | ^^^^^',
' 32 | const user = await login(email, password);',
' 33 | ',
' 33 |',
" 34 | if (user.role === 'admin') {",
].join('\n'),
]);
Expand Down Expand Up @@ -127,34 +140,39 @@ describe('EsLint', () => {
'WARNING in src/model/User.ts:11:5',
"@typescript-eslint/no-unused-vars: 'temporary' is defined but never used.",
' 9 | }',
' 10 | ',
' 10 |',
' > 11 | let temporary: any;',
' | ^^^^^^^^^^^^^^',
' 12 | ',
' 13 | ',
' 12 |',
' 13 |',
' 14 | function getUserName(user: User): string {',
].join('\n'),
[
'WARNING in src/model/User.ts:11:16',
'@typescript-eslint/no-explicit-any: Unexpected any. Specify a different type.',
' 9 | }',
' 10 | ',
' 10 |',
' > 11 | let temporary: any;',
' | ^^^',
' 12 | ',
' 13 | ',
' 12 |',
' 13 |',
' 14 | function getUserName(user: User): string {',
].join('\n'),
]);
});

it('adds files dependencies to webpack', async () => {
if (ignored) {
console.warn('Ignoring test - incompatible node version');
return;
}
await sandbox.load([
await readFixture(join(__dirname, 'fixtures/environment/eslint-basic.fixture'), {
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION: JSON.stringify(
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION
),
TS_LOADER_VERSION: JSON.stringify('^5.0.0'),
ESLINT_VERSION: JSON.stringify('~6.8.0'),
TYPESCRIPT_VERSION: JSON.stringify('~3.8.0'),
WEBPACK_VERSION: JSON.stringify('^4.0.0'),
WEBPACK_CLI_VERSION: JSON.stringify(WEBPACK_CLI_VERSION),
Expand Down Expand Up @@ -210,54 +228,65 @@ describe('EsLint', () => {
await driver.waitForNoErrors();
});

it('fixes errors with `fix: true` option', async () => {
await sandbox.load([
await readFixture(join(__dirname, 'fixtures/environment/eslint-basic.fixture'), {
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION: JSON.stringify(
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION
),
TS_LOADER_VERSION: JSON.stringify('^5.0.0'),
TYPESCRIPT_VERSION: JSON.stringify('~3.8.0'),
WEBPACK_VERSION: JSON.stringify('^4.0.0'),
WEBPACK_CLI_VERSION: JSON.stringify(WEBPACK_CLI_VERSION),
WEBPACK_DEV_SERVER_VERSION: JSON.stringify(WEBPACK_DEV_SERVER_VERSION),
ASYNC: JSON.stringify(false),
}),
await readFixture(join(__dirname, 'fixtures/implementation/typescript-basic.fixture')),
]);

// fix initial issues
await sandbox.patch(
'src/authenticate.ts',
'async function logout(): Promise<any> {',
'async function logout(): Promise<unknown> {'
);
await sandbox.patch(
'src/index.ts',
"loginForm.addEventListener('submit', async event => {",
"loginForm.addEventListener('submit', async () => {"
);
it.each([{ eslint: '^6.0.0' }, { eslint: '^7.0.0' }, { eslint: '^8.0.0' }])(
'fixes errors with `fix: true` option for %p',
async ({ eslint }) => {
if (ignored) {
console.warn('Ignoring test - incompatible node version');
return;
}
await sandbox.load([
await readFixture(join(__dirname, 'fixtures/environment/eslint-basic.fixture'), {
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION: JSON.stringify(
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION
),
TS_LOADER_VERSION: JSON.stringify('^5.0.0'),
ESLINT_VERSION: JSON.stringify(eslint),
TYPESCRIPT_VERSION: JSON.stringify('~3.8.0'),
WEBPACK_VERSION: JSON.stringify('^4.0.0'),
WEBPACK_CLI_VERSION: JSON.stringify(WEBPACK_CLI_VERSION),
WEBPACK_DEV_SERVER_VERSION: JSON.stringify(WEBPACK_DEV_SERVER_VERSION),
ASYNC: JSON.stringify(false),
}),
await readFixture(join(__dirname, 'fixtures/implementation/typescript-basic.fixture')),
]);

// fix initial issues
await sandbox.patch(
'src/authenticate.ts',
'async function logout(): Promise<any> {',
'async function logout(): Promise<unknown> {'
);
await sandbox.patch(
'src/index.ts',
"loginForm.addEventListener('submit', async event => {",
"loginForm.addEventListener('submit', async () => {"
);

// set fix option for the eslint
await sandbox.write(
'fork-ts-checker.config.js',
'module.exports = { eslint: { enabled: true, options: { fix: true } } };'
);
// set fix option for the eslint
await sandbox.write(
'fork-ts-checker.config.js',
'module.exports = { eslint: { enabled: true, options: { fix: true } } };'
);

// add fixable issue
await sandbox.patch(
'src/authenticate.ts',
'const response = await fetch(',
'let response = await fetch('
);
// add fixable issue
await sandbox.patch(
'src/authenticate.ts',
'const response = await fetch(',
'let response = await fetch('
);

const driver = createWebpackDevServerDriver(sandbox.spawn('npm run webpack-dev-server'), false);
const driver = createWebpackDevServerDriver(
sandbox.spawn('npm run webpack-dev-server'),
false
);

// it should be automatically fixed
await driver.waitForNoErrors();
// it should be automatically fixed
await driver.waitForNoErrors();

// check if issue has been fixed
const content = await sandbox.read('src/authenticate.ts');
expect(content).not.toContain('let response = await fetch(');
});
// check if issue has been fixed
const content = await sandbox.read('src/authenticate.ts');
expect(content).not.toContain('let response = await fetch(');
}
);
});
Loading

0 comments on commit 551f2fb

Please sign in to comment.