linter.js 7.9 KB

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