stringFormatter.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. 'use strict';
  2. const path = require('path');
  3. const stringWidth = require('string-width');
  4. const table = require('table');
  5. const { yellow, dim, underline, blue, red, green } = require('picocolors');
  6. const calcSeverityCounts = require('./calcSeverityCounts');
  7. const pluralize = require('../utils/pluralize');
  8. const { assertNumber } = require('../utils/validateTypes');
  9. const preprocessWarnings = require('./preprocessWarnings');
  10. const terminalLink = require('./terminalLink');
  11. const MARGIN_WIDTHS = 9;
  12. /**
  13. * @param {string} s
  14. * @returns {string}
  15. */
  16. function nope(s) {
  17. return s;
  18. }
  19. const levelColors = {
  20. info: blue,
  21. warning: yellow,
  22. error: red,
  23. success: nope,
  24. };
  25. const symbols = {
  26. info: blue('ℹ'),
  27. warning: yellow('⚠'),
  28. error: red('✖'),
  29. success: green('✔'),
  30. };
  31. /**
  32. * @param {import('stylelint').LintResult[]} results
  33. * @returns {string}
  34. */
  35. function deprecationsFormatter(results) {
  36. const allDeprecationWarnings = results.flatMap((result) => result.deprecations || []);
  37. if (allDeprecationWarnings.length === 0) {
  38. return '';
  39. }
  40. const seenText = new Set();
  41. const lines = [];
  42. for (const { text, reference } of allDeprecationWarnings) {
  43. if (seenText.has(text)) continue;
  44. seenText.add(text);
  45. let line = ` ${dim('-')} ${text}`;
  46. if (reference) {
  47. line += dim(` See: ${underline(reference)}`);
  48. }
  49. lines.push(line);
  50. }
  51. return ['', yellow('Deprecation warnings:'), ...lines, ''].join('\n');
  52. }
  53. /**
  54. * @param {import('stylelint').LintResult[]} results
  55. * @return {string}
  56. */
  57. function invalidOptionsFormatter(results) {
  58. const allInvalidOptionWarnings = results.flatMap((result) =>
  59. (result.invalidOptionWarnings || []).map((warning) => warning.text),
  60. );
  61. const uniqueInvalidOptionWarnings = [...new Set(allInvalidOptionWarnings)];
  62. return uniqueInvalidOptionWarnings.reduce((output, warning) => {
  63. output += red('Invalid Option: ');
  64. output += warning;
  65. return `${output}\n`;
  66. }, '\n');
  67. }
  68. /**
  69. * @param {string} fromValue
  70. * @param {string} cwd
  71. * @return {string}
  72. */
  73. function logFrom(fromValue, cwd) {
  74. if (fromValue.startsWith('<')) {
  75. return underline(fromValue);
  76. }
  77. const filePath = path.relative(cwd, fromValue).split(path.sep).join('/');
  78. return terminalLink(filePath, `file://${fromValue}`);
  79. }
  80. /**
  81. * @param {{[k: number]: number}} columnWidths
  82. * @return {number}
  83. */
  84. function getMessageWidth(columnWidths) {
  85. const width = columnWidths[3];
  86. assertNumber(width);
  87. if (!process.stdout.isTTY) {
  88. return width;
  89. }
  90. const availableWidth = process.stdout.columns < 80 ? 80 : process.stdout.columns;
  91. const fullWidth = Object.values(columnWidths).reduce((a, b) => a + b);
  92. // If there is no reason to wrap the text, we won't align the last column to the right
  93. if (availableWidth > fullWidth + MARGIN_WIDTHS) {
  94. return width;
  95. }
  96. return availableWidth - (fullWidth - width + MARGIN_WIDTHS);
  97. }
  98. /**
  99. * @param {import('stylelint').Warning[]} messages
  100. * @param {string} source
  101. * @param {string} cwd
  102. * @return {string}
  103. */
  104. function formatter(messages, source, cwd) {
  105. if (messages.length === 0) return '';
  106. /**
  107. * Create a list of column widths, needed to calculate
  108. * the size of the message column and if needed wrap it.
  109. * @type {{[k: string]: number}}
  110. */
  111. const columnWidths = { 0: 1, 1: 1, 2: 1, 3: 1, 4: 1 };
  112. /**
  113. * @param {[string, string, string, string, string]} columns
  114. * @return {[string, string, string, string, string]}
  115. */
  116. function calculateWidths(columns) {
  117. for (const [key, value] of Object.entries(columns)) {
  118. const normalisedValue = value ? value.toString() : value;
  119. const width = columnWidths[key];
  120. assertNumber(width);
  121. columnWidths[key] = Math.max(width, stringWidth(normalisedValue));
  122. }
  123. return columns;
  124. }
  125. let output = '\n';
  126. if (source) {
  127. output += `${logFrom(source, cwd)}\n`;
  128. }
  129. /**
  130. * @param {import('stylelint').Warning} message
  131. * @return {string}
  132. */
  133. function formatMessageText(message) {
  134. let result = message.text;
  135. result = result
  136. // Remove all control characters (newline, tab and etc)
  137. .replace(/[\u0001-\u001A]+/g, ' ') // eslint-disable-line no-control-regex
  138. .replace(/\.$/, '');
  139. const ruleString = ` (${message.rule})`;
  140. if (result.endsWith(ruleString)) {
  141. result = result.slice(0, result.lastIndexOf(ruleString));
  142. }
  143. return result;
  144. }
  145. const cleanedMessages = messages.map((message) => {
  146. const { line, column, severity } = message;
  147. /**
  148. * @type {[string, string, string, string, string]}
  149. */
  150. const row = [
  151. line ? line.toString() : '',
  152. column ? column.toString() : '',
  153. symbols[severity] ? levelColors[severity](symbols[severity]) : severity,
  154. formatMessageText(message),
  155. dim(message.rule || ''),
  156. ];
  157. calculateWidths(row);
  158. return row;
  159. });
  160. output += table
  161. .table(cleanedMessages, {
  162. border: table.getBorderCharacters('void'),
  163. columns: {
  164. 0: { alignment: 'right', width: columnWidths[0], paddingRight: 0 },
  165. 1: { alignment: 'left', width: columnWidths[1] },
  166. 2: { alignment: 'center', width: columnWidths[2] },
  167. 3: {
  168. alignment: 'left',
  169. width: getMessageWidth(columnWidths),
  170. wrapWord: getMessageWidth(columnWidths) > 1,
  171. },
  172. 4: { alignment: 'left', width: columnWidths[4], paddingRight: 0 },
  173. },
  174. drawHorizontalLine: () => false,
  175. })
  176. .split('\n')
  177. .map((el) => el.replace(/(\d+)\s+(\d+)/, (_m, p1, p2) => dim(`${p1}:${p2}`)).trimEnd())
  178. .join('\n');
  179. return output;
  180. }
  181. /**
  182. * @type {import('stylelint').Formatter}
  183. */
  184. module.exports = function stringFormatter(results, returnValue) {
  185. let output = invalidOptionsFormatter(results);
  186. output += deprecationsFormatter(results);
  187. const counts = { error: 0, warning: 0 };
  188. output = results.reduce((accum, result) => {
  189. preprocessWarnings(result);
  190. accum += formatter(
  191. result.warnings,
  192. result.source || '',
  193. (returnValue && returnValue.cwd) || process.cwd(),
  194. );
  195. for (const warning of result.warnings) {
  196. calcSeverityCounts(warning.severity, counts);
  197. }
  198. return accum;
  199. }, output);
  200. // Ensure consistent padding
  201. output = output.trim();
  202. if (output !== '') {
  203. output = `\n${output}\n\n`;
  204. const errorCount = counts.error;
  205. const warningCount = counts.warning;
  206. const total = errorCount + warningCount;
  207. if (total > 0) {
  208. const error = red(`${errorCount} ${pluralize('error', errorCount)}`);
  209. const warning = yellow(`${warningCount} ${pluralize('warning', warningCount)}`);
  210. const tally = `${total} ${pluralize('problem', total)} (${error}, ${warning})`;
  211. output += `${tally}\n\n`;
  212. }
  213. }
  214. return output;
  215. };