342 lines
7.7 KiB
JavaScript
342 lines
7.7 KiB
JavaScript
|
"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<void>} GenerateReport */
|
||
|
|
||
|
/** @typedef {{errors?: ESLintError, warnings?: ESLintError, generateReportAsset?: GenerateReport}} Report */
|
||
|
|
||
|
/** @typedef {() => Promise<Report>} Reporter */
|
||
|
|
||
|
/** @typedef {(files: string|string[]) => void} Linter */
|
||
|
|
||
|
/** @typedef {{[files: string]: LintResult}} LintResultMap */
|
||
|
|
||
|
/** @type {WeakMap<Compiler, LintResultMap>} */
|
||
|
|
||
|
|
||
|
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<LintResult[]>} */
|
||
|
|
||
|
let lintFiles;
|
||
|
/** @type {() => Promise<void>} */
|
||
|
|
||
|
let cleanup;
|
||
|
/** @type number */
|
||
|
|
||
|
let threads;
|
||
|
/** @type {Promise<LintResult[]>[]} */
|
||
|
|
||
|
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<void>}
|
||
|
*/
|
||
|
|
||
|
async function generateReportAsset({
|
||
|
compiler
|
||
|
}) {
|
||
|
const {
|
||
|
outputReport
|
||
|
} = options;
|
||
|
/**
|
||
|
* @param {string} name
|
||
|
* @param {string | Buffer} content
|
||
|
*/
|
||
|
|
||
|
const save = (name, content) =>
|
||
|
/** @type {Promise<void>} */
|
||
|
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<Formatter>}
|
||
|
*/
|
||
|
|
||
|
|
||
|
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<LintResult[]>}
|
||
|
*/
|
||
|
|
||
|
|
||
|
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<LintResult[]>[]} results
|
||
|
* @returns {Promise<LintResult[]>}
|
||
|
*/
|
||
|
|
||
|
|
||
|
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;
|