Skip to content

Commit

Permalink
feat: add support for eslint 8
Browse files Browse the repository at this point in the history
Closes: #664
  • Loading branch information
piotr-oles committed Oct 20, 2021
1 parent 34ebcd8 commit 948078f
Show file tree
Hide file tree
Showing 13 changed files with 14,243 additions and 4,281 deletions.
87 changes: 70 additions & 17 deletions src/eslint-reporter/reporter/EsLintReporter.ts
@@ -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
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
52 changes: 38 additions & 14 deletions test/e2e/EsLint.spec.ts
@@ -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,28 @@ 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, ignored },
{ async: true, webpack: '^4.0.0', eslint: '^7.0.0', absolute: true, ignored },
{ async: false, webpack: '^5.0.0', eslint: '^7.0.0', absolute: true, ignored },
{
async: true,
webpack: '^5.0.0',
eslint: '^8.0.0',
absolute: false,
ignored,
},
])('reports lint error for %p', async ({ async, webpack, eslint, absolute, ignored }) => {
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 +75,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 +90,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,22 +141,22 @@ 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'),
]);
Expand All @@ -155,6 +169,7 @@ describe('EsLint', () => {
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,13 +225,22 @@ describe('EsLint', () => {
await driver.waitForNoErrors();
});

it('fixes errors with `fix: true` option', async () => {
it.each([
{ eslint: '^6.0.0', ignored },
{ eslint: '^7.0.0', ignored },
{ eslint: '^8.0.0', ignored },
])('fixes errors with `fix: true` option for %p', async ({ eslint, ignored }) => {
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),
Expand Down
16 changes: 8 additions & 8 deletions test/e2e/fixtures/environment/eslint-basic.fixture
Expand Up @@ -9,10 +9,10 @@
},
"devDependencies": {
"@types/eslint": "^6.8.0",
"@typescript-eslint/eslint-plugin": "^2.27.0",
"@typescript-eslint/parser": "^2.27.0",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"css-loader": "^3.5.0",
"eslint": "^6.8.0",
"eslint": ${ESLINT_VERSION},
"fork-ts-checker-webpack-plugin": ${FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION},
"style-loader": "^1.2.0",
"ts-loader": ${TS_LOADER_VERSION},
Expand Down Expand Up @@ -44,11 +44,11 @@
/// .eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module'
},
extends: ['plugin:@typescript-eslint/recommended']
plugins: ["@typescript-eslint"],
extends: ["plugin:@typescript-eslint/recommended"],
rules: {
'@typescript-eslint/no-loss-of-precision': 'off'
}
};

/// webpack.config.js
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/jest.setup.js
@@ -1 +1 @@
jest.retryTimes(5);
// jest.retryTimes(5);

0 comments on commit 948078f

Please sign in to comment.