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