You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
342 lines
7.7 KiB
342 lines
7.7 KiB
"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; |