linter.js 7.9 KB

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