verboseFormatter.mjs 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import picocolors from 'picocolors';
  2. const { underline, red, yellow, dim, green } = picocolors;
  3. import pluralize from '../utils/pluralize.mjs';
  4. import stringFormatter from './stringFormatter.mjs';
  5. import terminalLink from './terminalLink.mjs';
  6. /** @import {Formatter, LintResult, RuleMeta, Severity, Warning} from 'stylelint' */
  7. /**
  8. * @type {Formatter}
  9. */
  10. export default function verboseFormatter(results, returnValue) {
  11. let output = stringFormatter(results, returnValue);
  12. if (output === '') {
  13. output = '\n';
  14. }
  15. const ignoredCount = results.filter((result) => result.ignored).length;
  16. const checkedDisplay = ignoredCount
  17. ? `${results.length - ignoredCount} of ${results.length}`
  18. : results.length;
  19. output += underline(`${checkedDisplay} ${pluralize('source', results.length)} checked\n`);
  20. for (const result of results) {
  21. let formatting = green;
  22. if (result.errored) {
  23. formatting = red;
  24. } else if (result.warnings.length) {
  25. formatting = yellow;
  26. } else if (result.ignored) {
  27. formatting = dim;
  28. }
  29. let sourceText = fileLink(result.source);
  30. if (result.ignored) {
  31. sourceText += ' (ignored)';
  32. }
  33. output += formatting(` ${sourceText}\n`);
  34. }
  35. const warnings = results.flatMap((r) => r.warnings);
  36. if (warnings.length === 0) {
  37. output += '\n0 problems found\n';
  38. } else {
  39. const warningsBySeverity = groupBy(warnings, (w) => w.severity);
  40. /**
  41. * @param {Severity} severity
  42. */
  43. const printProblems = (severity) => {
  44. const problems = warningsBySeverity[severity];
  45. if (problems === undefined) return;
  46. output += '\n';
  47. output += underline(`${problems.length} ${pluralize(severity, problems.length)} found\n`);
  48. const problemsByRule = groupBy(problems, (w) => w.rule);
  49. const metadata = returnValue.ruleMetadata;
  50. for (const [rule, list] of Object.entries(problemsByRule)) {
  51. const meta = metadata[rule] || {};
  52. let additional = [meta.fixable ? 'maybe fixable' : '', meta.deprecated ? 'deprecated' : '']
  53. .filter(Boolean)
  54. .join(', ');
  55. additional = additional ? ` (${additional})` : '';
  56. output += dim(` ${ruleLink(rule, meta)}: ${list.length}${additional}\n`);
  57. }
  58. };
  59. printProblems('error');
  60. printProblems('warning');
  61. }
  62. const fixedRules = getFixedRules(results);
  63. if (fixedRules.size) {
  64. let lines = '\n';
  65. let total = 0;
  66. for (const [name, count] of fixedRules) {
  67. const meta = returnValue.ruleMetadata[name];
  68. lines += dim(` ${ruleLink(name, meta)}: ${count}\n`);
  69. total += count;
  70. }
  71. output += '\n';
  72. output += underline(`${total} ${pluralize('problem', total)} fixed`);
  73. output += lines;
  74. }
  75. return `${output}\n`;
  76. }
  77. /**
  78. * @template {string} K
  79. * @param {Warning[]} array
  80. * @param {(w: Warning) => K} keyFn
  81. * @returns {Record<K, Warning[]>}
  82. * @todo replace by Object.groupBy once support for Node.js version 20 is dropped
  83. */
  84. function groupBy(array, keyFn) {
  85. /** @type {Record<string, Warning[]>} */
  86. const result = {};
  87. for (const item of array) {
  88. const key = keyFn(item);
  89. let warnings = result[key];
  90. if (warnings === undefined) {
  91. result[key] = warnings = [];
  92. }
  93. warnings.push(item);
  94. }
  95. return result;
  96. }
  97. /**
  98. * @param {string | undefined} source
  99. * @returns {string}
  100. */
  101. function fileLink(source) {
  102. if (!source || source.startsWith('<')) {
  103. return `${source}`;
  104. }
  105. return terminalLink(source, `file://${source}`);
  106. }
  107. /**
  108. * @param {string} rule
  109. * @param {Partial<RuleMeta> | undefined} metadata
  110. * @returns {string}
  111. */
  112. function ruleLink(rule, metadata) {
  113. if (metadata && metadata.url) {
  114. return terminalLink(rule, metadata.url);
  115. }
  116. return rule;
  117. }
  118. /** @param {LintResult[]} results */
  119. function getFixedRules(results) {
  120. /** @type {Map<string, number>} */
  121. const rules = new Map();
  122. for (const { _postcssResult } of results) {
  123. if (!_postcssResult) continue; // CSS syntax error
  124. const {
  125. stylelint: { fixersData },
  126. } = _postcssResult;
  127. const entries = Object.entries(fixersData);
  128. for (const [ruleName, count] of entries) {
  129. if (count) rules.set(ruleName, count);
  130. }
  131. }
  132. return rules;
  133. }