| 1 | "use strict";
|
|---|
| 2 |
|
|---|
| 3 | const {
|
|---|
| 4 | dirname,
|
|---|
| 5 | isAbsolute,
|
|---|
| 6 | join
|
|---|
| 7 | } = require('path');
|
|---|
| 8 |
|
|---|
| 9 | const ESLintError = require('./ESLintError');
|
|---|
| 10 |
|
|---|
| 11 | const {
|
|---|
| 12 | getESLint
|
|---|
| 13 | } = require('./getESLint');
|
|---|
| 14 | /** @typedef {import('eslint').ESLint} ESLint */
|
|---|
| 15 |
|
|---|
| 16 | /** @typedef {import('eslint').ESLint.Formatter} Formatter */
|
|---|
| 17 |
|
|---|
| 18 | /** @typedef {import('eslint').ESLint.LintResult} LintResult */
|
|---|
| 19 |
|
|---|
| 20 | /** @typedef {import('webpack').Compiler} Compiler */
|
|---|
| 21 |
|
|---|
| 22 | /** @typedef {import('webpack').Compilation} Compilation */
|
|---|
| 23 |
|
|---|
| 24 | /** @typedef {import('./options').Options} Options */
|
|---|
| 25 |
|
|---|
| 26 | /** @typedef {import('./options').FormatterFunction} FormatterFunction */
|
|---|
| 27 |
|
|---|
| 28 | /** @typedef {(compilation: Compilation) => Promise<void>} GenerateReport */
|
|---|
| 29 |
|
|---|
| 30 | /** @typedef {{errors?: ESLintError, warnings?: ESLintError, generateReportAsset?: GenerateReport}} Report */
|
|---|
| 31 |
|
|---|
| 32 | /** @typedef {() => Promise<Report>} Reporter */
|
|---|
| 33 |
|
|---|
| 34 | /** @typedef {(files: string|string[]) => void} Linter */
|
|---|
| 35 |
|
|---|
| 36 | /** @typedef {{[files: string]: LintResult}} LintResultMap */
|
|---|
| 37 |
|
|---|
| 38 | /** @type {WeakMap<Compiler, LintResultMap>} */
|
|---|
| 39 |
|
|---|
| 40 |
|
|---|
| 41 | const resultStorage = new WeakMap();
|
|---|
| 42 | /**
|
|---|
| 43 | * @param {string|undefined} key
|
|---|
| 44 | * @param {Options} options
|
|---|
| 45 | * @param {Compilation} compilation
|
|---|
| 46 | * @returns {{lint: Linter, report: Reporter, threads: number}}
|
|---|
| 47 | */
|
|---|
| 48 |
|
|---|
| 49 | function linter(key, options, compilation) {
|
|---|
| 50 | /** @type {ESLint} */
|
|---|
| 51 | let eslint;
|
|---|
| 52 | /** @type {(files: string|string[]) => Promise<LintResult[]>} */
|
|---|
| 53 |
|
|---|
| 54 | let lintFiles;
|
|---|
| 55 | /** @type {() => Promise<void>} */
|
|---|
| 56 |
|
|---|
| 57 | let cleanup;
|
|---|
| 58 | /** @type number */
|
|---|
| 59 |
|
|---|
| 60 | let threads;
|
|---|
| 61 | /** @type {Promise<LintResult[]>[]} */
|
|---|
| 62 |
|
|---|
| 63 | const rawResults = [];
|
|---|
| 64 | const crossRunResultStorage = getResultStorage(compilation);
|
|---|
| 65 |
|
|---|
| 66 | try {
|
|---|
| 67 | ({
|
|---|
| 68 | eslint,
|
|---|
| 69 | lintFiles,
|
|---|
| 70 | cleanup,
|
|---|
| 71 | threads
|
|---|
| 72 | } = getESLint(key, options));
|
|---|
| 73 | } catch (e) {
|
|---|
| 74 | throw new ESLintError(e.message);
|
|---|
| 75 | }
|
|---|
| 76 |
|
|---|
| 77 | return {
|
|---|
| 78 | lint,
|
|---|
| 79 | report,
|
|---|
| 80 | threads
|
|---|
| 81 | };
|
|---|
| 82 | /**
|
|---|
| 83 | * @param {string | string[]} files
|
|---|
| 84 | */
|
|---|
| 85 |
|
|---|
| 86 | function lint(files) {
|
|---|
| 87 | for (const file of asList(files)) {
|
|---|
| 88 | delete crossRunResultStorage[file];
|
|---|
| 89 | }
|
|---|
| 90 |
|
|---|
| 91 | rawResults.push(lintFiles(files).catch(e => {
|
|---|
| 92 | // @ts-ignore
|
|---|
| 93 | compilation.errors.push(new ESLintError(e.message));
|
|---|
| 94 | return [];
|
|---|
| 95 | }));
|
|---|
| 96 | }
|
|---|
| 97 |
|
|---|
| 98 | async function report() {
|
|---|
| 99 | // Filter out ignored files.
|
|---|
| 100 | let results = await removeIgnoredWarnings(eslint, // Get the current results, resetting the rawResults to empty
|
|---|
| 101 | await flatten(rawResults.splice(0, rawResults.length)));
|
|---|
| 102 | await cleanup();
|
|---|
| 103 |
|
|---|
| 104 | for (const result of results) {
|
|---|
| 105 | crossRunResultStorage[result.filePath] = result;
|
|---|
| 106 | }
|
|---|
| 107 |
|
|---|
| 108 | results = Object.values(crossRunResultStorage); // do not analyze if there are no results or eslint config
|
|---|
| 109 |
|
|---|
| 110 | if (!results || results.length < 1) {
|
|---|
| 111 | return {};
|
|---|
| 112 | }
|
|---|
| 113 |
|
|---|
| 114 | const formatter = await loadFormatter(eslint, options.formatter);
|
|---|
| 115 | const {
|
|---|
| 116 | errors,
|
|---|
| 117 | warnings
|
|---|
| 118 | } = await formatResults(formatter, parseResults(options, results));
|
|---|
| 119 | return {
|
|---|
| 120 | errors,
|
|---|
| 121 | warnings,
|
|---|
| 122 | generateReportAsset
|
|---|
| 123 | };
|
|---|
| 124 | /**
|
|---|
| 125 | * @param {Compilation} compilation
|
|---|
| 126 | * @returns {Promise<void>}
|
|---|
| 127 | */
|
|---|
| 128 |
|
|---|
| 129 | async function generateReportAsset({
|
|---|
| 130 | compiler
|
|---|
| 131 | }) {
|
|---|
| 132 | const {
|
|---|
| 133 | outputReport
|
|---|
| 134 | } = options;
|
|---|
| 135 | /**
|
|---|
| 136 | * @param {string} name
|
|---|
| 137 | * @param {string | Buffer} content
|
|---|
| 138 | */
|
|---|
| 139 |
|
|---|
| 140 | const save = (name, content) =>
|
|---|
| 141 | /** @type {Promise<void>} */
|
|---|
| 142 | new Promise((finish, bail) => {
|
|---|
| 143 | const {
|
|---|
| 144 | mkdir,
|
|---|
| 145 | writeFile
|
|---|
| 146 | } = compiler.outputFileSystem; // ensure directory exists
|
|---|
| 147 | // @ts-ignore - the types for `outputFileSystem` are missing the 3 arg overload
|
|---|
| 148 |
|
|---|
| 149 | mkdir(dirname(name), {
|
|---|
| 150 | recursive: true
|
|---|
| 151 | }, err => {
|
|---|
| 152 | /* istanbul ignore if */
|
|---|
| 153 | if (err) bail(err);else writeFile(name, content, err2 => {
|
|---|
| 154 | /* istanbul ignore if */
|
|---|
| 155 | if (err2) bail(err2);else finish();
|
|---|
| 156 | });
|
|---|
| 157 | });
|
|---|
| 158 | });
|
|---|
| 159 |
|
|---|
| 160 | if (!outputReport || !outputReport.filePath) {
|
|---|
| 161 | return;
|
|---|
| 162 | }
|
|---|
| 163 |
|
|---|
| 164 | const content = await (outputReport.formatter ? (await loadFormatter(eslint, outputReport.formatter)).format(results) : formatter.format(results));
|
|---|
| 165 | let {
|
|---|
| 166 | filePath
|
|---|
| 167 | } = outputReport;
|
|---|
| 168 |
|
|---|
| 169 | if (!isAbsolute(filePath)) {
|
|---|
| 170 | filePath = join(compiler.outputPath, filePath);
|
|---|
| 171 | }
|
|---|
| 172 |
|
|---|
| 173 | await save(filePath, content);
|
|---|
| 174 | }
|
|---|
| 175 | }
|
|---|
| 176 | }
|
|---|
| 177 | /**
|
|---|
| 178 | * @param {Formatter} formatter
|
|---|
| 179 | * @param {{ errors: LintResult[]; warnings: LintResult[]; }} results
|
|---|
| 180 | * @returns {Promise<{errors?: ESLintError, warnings?: ESLintError}>}
|
|---|
| 181 | */
|
|---|
| 182 |
|
|---|
| 183 |
|
|---|
| 184 | async function formatResults(formatter, results) {
|
|---|
| 185 | let errors;
|
|---|
| 186 | let warnings;
|
|---|
| 187 |
|
|---|
| 188 | if (results.warnings.length > 0) {
|
|---|
| 189 | warnings = new ESLintError(await formatter.format(results.warnings));
|
|---|
| 190 | }
|
|---|
| 191 |
|
|---|
| 192 | if (results.errors.length > 0) {
|
|---|
| 193 | errors = new ESLintError(await formatter.format(results.errors));
|
|---|
| 194 | }
|
|---|
| 195 |
|
|---|
| 196 | return {
|
|---|
| 197 | errors,
|
|---|
| 198 | warnings
|
|---|
| 199 | };
|
|---|
| 200 | }
|
|---|
| 201 | /**
|
|---|
| 202 | * @param {Options} options
|
|---|
| 203 | * @param {LintResult[]} results
|
|---|
| 204 | * @returns {{errors: LintResult[], warnings: LintResult[]}}
|
|---|
| 205 | */
|
|---|
| 206 |
|
|---|
| 207 |
|
|---|
| 208 | function parseResults(options, results) {
|
|---|
| 209 | /** @type {LintResult[]} */
|
|---|
| 210 | const errors = [];
|
|---|
| 211 | /** @type {LintResult[]} */
|
|---|
| 212 |
|
|---|
| 213 | const warnings = [];
|
|---|
| 214 | results.forEach(file => {
|
|---|
| 215 | if (fileHasErrors(file)) {
|
|---|
| 216 | const messages = file.messages.filter(message => options.emitError && message.severity === 2);
|
|---|
| 217 |
|
|---|
| 218 | if (messages.length > 0) {
|
|---|
| 219 | errors.push({ ...file,
|
|---|
| 220 | messages
|
|---|
| 221 | });
|
|---|
| 222 | }
|
|---|
| 223 | }
|
|---|
| 224 |
|
|---|
| 225 | if (fileHasWarnings(file)) {
|
|---|
| 226 | const messages = file.messages.filter(message => options.emitWarning && message.severity === 1);
|
|---|
| 227 |
|
|---|
| 228 | if (messages.length > 0) {
|
|---|
| 229 | warnings.push({ ...file,
|
|---|
| 230 | messages
|
|---|
| 231 | });
|
|---|
| 232 | }
|
|---|
| 233 | }
|
|---|
| 234 | });
|
|---|
| 235 | return {
|
|---|
| 236 | errors,
|
|---|
| 237 | warnings
|
|---|
| 238 | };
|
|---|
| 239 | }
|
|---|
| 240 | /**
|
|---|
| 241 | * @param {LintResult} file
|
|---|
| 242 | * @returns {boolean}
|
|---|
| 243 | */
|
|---|
| 244 |
|
|---|
| 245 |
|
|---|
| 246 | function fileHasErrors(file) {
|
|---|
| 247 | return file.errorCount > 0;
|
|---|
| 248 | }
|
|---|
| 249 | /**
|
|---|
| 250 | * @param {LintResult} file
|
|---|
| 251 | * @returns {boolean}
|
|---|
| 252 | */
|
|---|
| 253 |
|
|---|
| 254 |
|
|---|
| 255 | function fileHasWarnings(file) {
|
|---|
| 256 | return file.warningCount > 0;
|
|---|
| 257 | }
|
|---|
| 258 | /**
|
|---|
| 259 | * @param {ESLint} eslint
|
|---|
| 260 | * @param {string|FormatterFunction=} formatter
|
|---|
| 261 | * @returns {Promise<Formatter>}
|
|---|
| 262 | */
|
|---|
| 263 |
|
|---|
| 264 |
|
|---|
| 265 | async function loadFormatter(eslint, formatter) {
|
|---|
| 266 | if (typeof formatter === 'function') {
|
|---|
| 267 | return {
|
|---|
| 268 | format: formatter
|
|---|
| 269 | };
|
|---|
| 270 | }
|
|---|
| 271 |
|
|---|
| 272 | if (typeof formatter === 'string') {
|
|---|
| 273 | try {
|
|---|
| 274 | return eslint.loadFormatter(formatter);
|
|---|
| 275 | } catch (_) {// Load the default formatter.
|
|---|
| 276 | }
|
|---|
| 277 | }
|
|---|
| 278 |
|
|---|
| 279 | return eslint.loadFormatter();
|
|---|
| 280 | }
|
|---|
| 281 | /**
|
|---|
| 282 | * @param {ESLint} eslint
|
|---|
| 283 | * @param {LintResult[]} results
|
|---|
| 284 | * @returns {Promise<LintResult[]>}
|
|---|
| 285 | */
|
|---|
| 286 |
|
|---|
| 287 |
|
|---|
| 288 | async function removeIgnoredWarnings(eslint, results) {
|
|---|
| 289 | const filterPromises = results.map(async result => {
|
|---|
| 290 | // Short circuit the call to isPathIgnored.
|
|---|
| 291 | // fatal is false for ignored file warnings.
|
|---|
| 292 | // ruleId is unset for internal ESLint errors.
|
|---|
| 293 | // line is unset for warnings not involving file contents.
|
|---|
| 294 | 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));
|
|---|
| 295 | return ignored ? false : result;
|
|---|
| 296 | }); // @ts-ignore
|
|---|
| 297 |
|
|---|
| 298 | return (await Promise.all(filterPromises)).filter(result => !!result);
|
|---|
| 299 | }
|
|---|
| 300 | /**
|
|---|
| 301 | * @param {Promise<LintResult[]>[]} results
|
|---|
| 302 | * @returns {Promise<LintResult[]>}
|
|---|
| 303 | */
|
|---|
| 304 |
|
|---|
| 305 |
|
|---|
| 306 | async function flatten(results) {
|
|---|
| 307 | /**
|
|---|
| 308 | * @param {LintResult[]} acc
|
|---|
| 309 | * @param {LintResult[]} list
|
|---|
| 310 | */
|
|---|
| 311 | const flat = (acc, list) => [...acc, ...list];
|
|---|
| 312 |
|
|---|
| 313 | return (await Promise.all(results)).reduce(flat, []);
|
|---|
| 314 | }
|
|---|
| 315 | /**
|
|---|
| 316 | * @param {Compilation} compilation
|
|---|
| 317 | * @returns {LintResultMap}
|
|---|
| 318 | */
|
|---|
| 319 |
|
|---|
| 320 |
|
|---|
| 321 | function getResultStorage({
|
|---|
| 322 | compiler
|
|---|
| 323 | }) {
|
|---|
| 324 | let storage = resultStorage.get(compiler);
|
|---|
| 325 |
|
|---|
| 326 | if (!storage) {
|
|---|
| 327 | resultStorage.set(compiler, storage = {});
|
|---|
| 328 | }
|
|---|
| 329 |
|
|---|
| 330 | return storage;
|
|---|
| 331 | }
|
|---|
| 332 | /**
|
|---|
| 333 | * @param {string | string[]} x
|
|---|
| 334 | */
|
|---|
| 335 |
|
|---|
| 336 |
|
|---|
| 337 | function asList(x) {
|
|---|
| 338 | /* istanbul ignore next */
|
|---|
| 339 | return Array.isArray(x) ? x : [x];
|
|---|
| 340 | }
|
|---|
| 341 |
|
|---|
| 342 | module.exports = linter; |
|---|