"use strict"; const { dirname, isAbsolute, join } = require('path'); const ESLintError = require('./ESLintError'); const { getESLint } = require('./getESLint'); /** @typedef {import('eslint').ESLint} ESLint */ /** @typedef {import('eslint').ESLint.Formatter} Formatter */ /** @typedef {import('eslint').ESLint.LintResult} LintResult */ /** @typedef {import('webpack').Compiler} Compiler */ /** @typedef {import('webpack').Compilation} Compilation */ /** @typedef {import('./options').Options} Options */ /** @typedef {import('./options').FormatterFunction} FormatterFunction */ /** @typedef {(compilation: Compilation) => Promise} GenerateReport */ /** @typedef {{errors?: ESLintError, warnings?: ESLintError, generateReportAsset?: GenerateReport}} Report */ /** @typedef {() => Promise} Reporter */ /** @typedef {(files: string|string[]) => void} Linter */ /** @typedef {{[files: string]: LintResult}} LintResultMap */ /** @type {WeakMap} */ const resultStorage = new WeakMap(); /** * @param {string|undefined} key * @param {Options} options * @param {Compilation} compilation * @returns {{lint: Linter, report: Reporter, threads: number}} */ function linter(key, options, compilation) { /** @type {ESLint} */ let eslint; /** @type {(files: string|string[]) => Promise} */ let lintFiles; /** @type {() => Promise} */ let cleanup; /** @type number */ let threads; /** @type {Promise[]} */ const rawResults = []; const crossRunResultStorage = getResultStorage(compilation); try { ({ eslint, lintFiles, cleanup, threads } = getESLint(key, options)); } catch (e) { throw new ESLintError(e.message); } return { lint, report, threads }; /** * @param {string | string[]} files */ function lint(files) { for (const file of asList(files)) { delete crossRunResultStorage[file]; } rawResults.push(lintFiles(files).catch(e => { // @ts-ignore compilation.errors.push(new ESLintError(e.message)); return []; })); } async function report() { // Filter out ignored files. let results = await removeIgnoredWarnings(eslint, // Get the current results, resetting the rawResults to empty await flatten(rawResults.splice(0, rawResults.length))); await cleanup(); for (const result of results) { crossRunResultStorage[result.filePath] = result; } results = Object.values(crossRunResultStorage); // do not analyze if there are no results or eslint config if (!results || results.length < 1) { return {}; } const formatter = await loadFormatter(eslint, options.formatter); const { errors, warnings } = await formatResults(formatter, parseResults(options, results)); return { errors, warnings, generateReportAsset }; /** * @param {Compilation} compilation * @returns {Promise} */ async function generateReportAsset({ compiler }) { const { outputReport } = options; /** * @param {string} name * @param {string | Buffer} content */ const save = (name, content) => /** @type {Promise} */ new Promise((finish, bail) => { const { mkdir, writeFile } = compiler.outputFileSystem; // ensure directory exists // @ts-ignore - the types for `outputFileSystem` are missing the 3 arg overload mkdir(dirname(name), { recursive: true }, err => { /* istanbul ignore if */ if (err) bail(err);else writeFile(name, content, err2 => { /* istanbul ignore if */ if (err2) bail(err2);else finish(); }); }); }); if (!outputReport || !outputReport.filePath) { return; } const content = await (outputReport.formatter ? (await loadFormatter(eslint, outputReport.formatter)).format(results) : formatter.format(results)); let { filePath } = outputReport; if (!isAbsolute(filePath)) { filePath = join(compiler.outputPath, filePath); } await save(filePath, content); } } } /** * @param {Formatter} formatter * @param {{ errors: LintResult[]; warnings: LintResult[]; }} results * @returns {Promise<{errors?: ESLintError, warnings?: ESLintError}>} */ async function formatResults(formatter, results) { let errors; let warnings; if (results.warnings.length > 0) { warnings = new ESLintError(await formatter.format(results.warnings)); } if (results.errors.length > 0) { errors = new ESLintError(await formatter.format(results.errors)); } return { errors, warnings }; } /** * @param {Options} options * @param {LintResult[]} results * @returns {{errors: LintResult[], warnings: LintResult[]}} */ function parseResults(options, results) { /** @type {LintResult[]} */ const errors = []; /** @type {LintResult[]} */ const warnings = []; results.forEach(file => { if (fileHasErrors(file)) { const messages = file.messages.filter(message => options.emitError && message.severity === 2); if (messages.length > 0) { errors.push({ ...file, messages }); } } if (fileHasWarnings(file)) { const messages = file.messages.filter(message => options.emitWarning && message.severity === 1); if (messages.length > 0) { warnings.push({ ...file, messages }); } } }); return { errors, warnings }; } /** * @param {LintResult} file * @returns {boolean} */ function fileHasErrors(file) { return file.errorCount > 0; } /** * @param {LintResult} file * @returns {boolean} */ function fileHasWarnings(file) { return file.warningCount > 0; } /** * @param {ESLint} eslint * @param {string|FormatterFunction=} formatter * @returns {Promise} */ async function loadFormatter(eslint, formatter) { if (typeof formatter === 'function') { return { format: formatter }; } if (typeof formatter === 'string') { try { return eslint.loadFormatter(formatter); } catch (_) {// Load the default formatter. } } return eslint.loadFormatter(); } /** * @param {ESLint} eslint * @param {LintResult[]} results * @returns {Promise} */ async function removeIgnoredWarnings(eslint, results) { const filterPromises = results.map(async result => { // Short circuit the call to isPathIgnored. // fatal is false for ignored file warnings. // ruleId is unset for internal ESLint errors. // line is unset for warnings not involving file contents. const ignored = result.messages.length === 0 || result.warningCount === 1 && result.errorCount === 0 && !result.messages[0].fatal && !result.messages[0].ruleId && !result.messages[0].line && (await eslint.isPathIgnored(result.filePath)); return ignored ? false : result; }); // @ts-ignore return (await Promise.all(filterPromises)).filter(result => !!result); } /** * @param {Promise[]} results * @returns {Promise} */ async function flatten(results) { /** * @param {LintResult[]} acc * @param {LintResult[]} list */ const flat = (acc, list) => [...acc, ...list]; return (await Promise.all(results)).reduce(flat, []); } /** * @param {Compilation} compilation * @returns {LintResultMap} */ function getResultStorage({ compiler }) { let storage = resultStorage.get(compiler); if (!storage) { resultStorage.set(compiler, storage = {}); } return storage; } /** * @param {string | string[]} x */ function asList(x) { /* istanbul ignore next */ return Array.isArray(x) ? x : [x]; } module.exports = linter;