| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- 'use strict';
- const path = require('path');
- const stringWidth = require('string-width');
- const table = require('table');
- const { yellow, dim, underline, blue, red, green } = require('picocolors');
- const calcSeverityCounts = require('./calcSeverityCounts');
- const pluralize = require('../utils/pluralize');
- const { assertNumber } = require('../utils/validateTypes');
- const preprocessWarnings = require('./preprocessWarnings');
- const terminalLink = require('./terminalLink');
- const MARGIN_WIDTHS = 9;
- /**
- * @param {string} s
- * @returns {string}
- */
- function nope(s) {
- return s;
- }
- const levelColors = {
- info: blue,
- warning: yellow,
- error: red,
- success: nope,
- };
- const symbols = {
- info: blue('ℹ'),
- warning: yellow('⚠'),
- error: red('✖'),
- success: green('✔'),
- };
- /**
- * @param {import('stylelint').LintResult[]} results
- * @returns {string}
- */
- function deprecationsFormatter(results) {
- const allDeprecationWarnings = results.flatMap((result) => result.deprecations || []);
- if (allDeprecationWarnings.length === 0) {
- return '';
- }
- const seenText = new Set();
- const lines = [];
- for (const { text, reference } of allDeprecationWarnings) {
- if (seenText.has(text)) continue;
- seenText.add(text);
- let line = ` ${dim('-')} ${text}`;
- if (reference) {
- line += dim(` See: ${underline(reference)}`);
- }
- lines.push(line);
- }
- return ['', yellow('Deprecation warnings:'), ...lines, ''].join('\n');
- }
- /**
- * @param {import('stylelint').LintResult[]} results
- * @return {string}
- */
- function invalidOptionsFormatter(results) {
- const allInvalidOptionWarnings = results.flatMap((result) =>
- (result.invalidOptionWarnings || []).map((warning) => warning.text),
- );
- const uniqueInvalidOptionWarnings = [...new Set(allInvalidOptionWarnings)];
- return uniqueInvalidOptionWarnings.reduce((output, warning) => {
- output += red('Invalid Option: ');
- output += warning;
- return `${output}\n`;
- }, '\n');
- }
- /**
- * @param {string} fromValue
- * @param {string} cwd
- * @return {string}
- */
- function logFrom(fromValue, cwd) {
- if (fromValue.startsWith('<')) {
- return underline(fromValue);
- }
- const filePath = path.relative(cwd, fromValue).split(path.sep).join('/');
- return terminalLink(filePath, `file://${fromValue}`);
- }
- /**
- * @param {{[k: number]: number}} columnWidths
- * @return {number}
- */
- function getMessageWidth(columnWidths) {
- const width = columnWidths[3];
- assertNumber(width);
- if (!process.stdout.isTTY) {
- return width;
- }
- const availableWidth = process.stdout.columns < 80 ? 80 : process.stdout.columns;
- const fullWidth = Object.values(columnWidths).reduce((a, b) => a + b);
- // If there is no reason to wrap the text, we won't align the last column to the right
- if (availableWidth > fullWidth + MARGIN_WIDTHS) {
- return width;
- }
- return availableWidth - (fullWidth - width + MARGIN_WIDTHS);
- }
- /**
- * @param {import('stylelint').Warning[]} messages
- * @param {string} source
- * @param {string} cwd
- * @return {string}
- */
- function formatter(messages, source, cwd) {
- if (messages.length === 0) return '';
- /**
- * Create a list of column widths, needed to calculate
- * the size of the message column and if needed wrap it.
- * @type {{[k: string]: number}}
- */
- const columnWidths = { 0: 1, 1: 1, 2: 1, 3: 1, 4: 1 };
- /**
- * @param {[string, string, string, string, string]} columns
- * @return {[string, string, string, string, string]}
- */
- function calculateWidths(columns) {
- for (const [key, value] of Object.entries(columns)) {
- const normalisedValue = value ? value.toString() : value;
- const width = columnWidths[key];
- assertNumber(width);
- columnWidths[key] = Math.max(width, stringWidth(normalisedValue));
- }
- return columns;
- }
- let output = '\n';
- if (source) {
- output += `${logFrom(source, cwd)}\n`;
- }
- /**
- * @param {import('stylelint').Warning} message
- * @return {string}
- */
- function formatMessageText(message) {
- let result = message.text;
- result = result
- // Remove all control characters (newline, tab and etc)
- .replace(/[\u0001-\u001A]+/g, ' ') // eslint-disable-line no-control-regex
- .replace(/\.$/, '');
- const ruleString = ` (${message.rule})`;
- if (result.endsWith(ruleString)) {
- result = result.slice(0, result.lastIndexOf(ruleString));
- }
- return result;
- }
- const cleanedMessages = messages.map((message) => {
- const { line, column, severity } = message;
- /**
- * @type {[string, string, string, string, string]}
- */
- const row = [
- line ? line.toString() : '',
- column ? column.toString() : '',
- symbols[severity] ? levelColors[severity](symbols[severity]) : severity,
- formatMessageText(message),
- dim(message.rule || ''),
- ];
- calculateWidths(row);
- return row;
- });
- output += table
- .table(cleanedMessages, {
- border: table.getBorderCharacters('void'),
- columns: {
- 0: { alignment: 'right', width: columnWidths[0], paddingRight: 0 },
- 1: { alignment: 'left', width: columnWidths[1] },
- 2: { alignment: 'center', width: columnWidths[2] },
- 3: {
- alignment: 'left',
- width: getMessageWidth(columnWidths),
- wrapWord: getMessageWidth(columnWidths) > 1,
- },
- 4: { alignment: 'left', width: columnWidths[4], paddingRight: 0 },
- },
- drawHorizontalLine: () => false,
- })
- .split('\n')
- .map((el) => el.replace(/(\d+)\s+(\d+)/, (_m, p1, p2) => dim(`${p1}:${p2}`)).trimEnd())
- .join('\n');
- return output;
- }
- /**
- * @type {import('stylelint').Formatter}
- */
- module.exports = function stringFormatter(results, returnValue) {
- let output = invalidOptionsFormatter(results);
- output += deprecationsFormatter(results);
- const counts = { error: 0, warning: 0 };
- output = results.reduce((accum, result) => {
- preprocessWarnings(result);
- accum += formatter(
- result.warnings,
- result.source || '',
- (returnValue && returnValue.cwd) || process.cwd(),
- );
- for (const warning of result.warnings) {
- calcSeverityCounts(warning.severity, counts);
- }
- return accum;
- }, output);
- // Ensure consistent padding
- output = output.trim();
- if (output !== '') {
- output = `\n${output}\n\n`;
- const errorCount = counts.error;
- const warningCount = counts.warning;
- const total = errorCount + warningCount;
- if (total > 0) {
- const error = red(`${errorCount} ${pluralize('error', errorCount)}`);
- const warning = yellow(`${warningCount} ${pluralize('warning', warningCount)}`);
- const tally = `${total} ${pluralize('problem', total)} (${error}, ${warning})`;
- output += `${tally}\n\n`;
- }
- }
- return output;
- };
|