diff --git a/.changeset/nice-carpets-yawn.md b/.changeset/nice-carpets-yawn.md new file mode 100644 index 0000000000..fe5015ed62 --- /dev/null +++ b/.changeset/nice-carpets-yawn.md @@ -0,0 +1,5 @@ +--- +"stylelint": patch +--- + +Fixed: incorrect output by all formatters except for `json` diff --git a/lib/formatters/__tests__/compactFormatter.test.js b/lib/formatters/__tests__/compactFormatter.test.js index 1ab4a4c67a..0d9989962d 100644 --- a/lib/formatters/__tests__/compactFormatter.test.js +++ b/lib/formatters/__tests__/compactFormatter.test.js @@ -21,10 +21,7 @@ describe('compactFormatter', () => { const results = [ { source: 'path/to/file.css', - errored: false, warnings: [], - deprecations: [], - invalidOptionWarnings: [], }, ]; @@ -37,7 +34,6 @@ describe('compactFormatter', () => { const results = [ { source: 'path/to/file.css', - errored: true, warnings: [ { line: 1, @@ -47,8 +43,6 @@ describe('compactFormatter', () => { text: 'Unexpected foo', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; @@ -63,7 +57,6 @@ describe('compactFormatter', () => { const results = [ { source: 'path/to/file.css', - errored: true, warnings: [ { line: 1, @@ -73,8 +66,6 @@ describe('compactFormatter', () => { text: 'Unexpected foo', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; @@ -83,7 +74,7 @@ describe('compactFormatter', () => { expect(output).toBe('path/to/file.css: line 1, col 1, error - Unexpected foo'); }); - it('output warnings with more than 80 characters and `process.stdout.columns` equal 90 characters', () => { + it('outputs warnings with more than 80 characters and `process.stdout.columns` equal 90 characters', () => { // For Windows tests process.stdout.isTTY = true; process.stdout.columns = 90; @@ -91,7 +82,6 @@ describe('compactFormatter', () => { const results = [ { source: 'path/to/file.css', - errored: true, warnings: [ { line: 1, @@ -101,8 +91,6 @@ describe('compactFormatter', () => { text: 'Unexpected very very very very very very very very very very very very very long foo', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; @@ -118,8 +106,6 @@ describe('compactFormatter', () => { { source: 'file.css', warnings: [], - deprecations: [], - invalidOptionWarnings: [], ignored: true, }, ]; @@ -128,4 +114,40 @@ describe('compactFormatter', () => { expect(output).toBe(''); }); + + it('outputs parse errors and warnings without rule and severity', () => { + const results = [ + { + source: 'path/to/file.css', + parseErrors: [ + { + line: 1, + column: 1, + stylelintType: 'foo-error', + text: 'Cannot parse foo', + }, + ], + warnings: [ + { + line: 3, + column: 2, + rule: 'bar', + severity: 'error', + text: 'Unexpected foo', + }, + { + line: 2, + column: 1, + text: 'Anonymous error', + }, + ], + }, + ]; + + const output = prepareFormatterOutput(results, compactFormatter); + + expect(output).toBe(`path/to/file.css: line 1, col 1, error - Cannot parse foo (foo-error) +path/to/file.css: line 2, col 1, error - Anonymous error +path/to/file.css: line 3, col 2, error - Unexpected foo`); + }); }); diff --git a/lib/formatters/__tests__/githubFormatter.test.js b/lib/formatters/__tests__/githubFormatter.test.js index 2adc6ddf71..123d1ade0e 100644 --- a/lib/formatters/__tests__/githubFormatter.test.js +++ b/lib/formatters/__tests__/githubFormatter.test.js @@ -28,6 +28,19 @@ test('githubFormatter', () => { severity: 'warning', text: 'Unexpected "bar" (bar)', }, + { + line: 20, + column: 3, + text: 'Anonymous error', + }, + ], + parseErrors: [ + { + line: 20, + column: 1, + stylelintType: 'foo-error', + text: 'Cannot parse foo', + }, ], }, ]; @@ -40,5 +53,7 @@ test('githubFormatter', () => { expect(githubFormatter(results, returnValue)) .toBe(`::error file=path/to/file.css,line=1,col=2,endLine=1,endColumn=5,title=Stylelint problem::Unexpected "foo" (foo) - https://stylelint.io/rules/foo -::warning file=a.css,line=10,col=20,title=Stylelint problem::Unexpected "bar" (bar) [maybe fixable]`); +::warning file=a.css,line=10,col=20,title=Stylelint problem::Unexpected "bar" (bar) [maybe fixable] +::error file=a.css,line=20,col=1,title=Stylelint problem::Cannot parse foo (foo-error) +::error file=a.css,line=20,col=3,title=Stylelint problem::Anonymous error`); }); diff --git a/lib/formatters/__tests__/stringFormatter.test.js b/lib/formatters/__tests__/stringFormatter.test.js index ee5c58d857..b6e1ba2c75 100644 --- a/lib/formatters/__tests__/stringFormatter.test.js +++ b/lib/formatters/__tests__/stringFormatter.test.js @@ -23,10 +23,7 @@ describe('stringFormatter', () => { const results = [ { source: 'path/to/file.css', - errored: false, warnings: [], - deprecations: [], - invalidOptionWarnings: [], }, ]; @@ -39,7 +36,6 @@ describe('stringFormatter', () => { const results = [ { source: 'path/to/file.css', - errored: true, warnings: [ { line: 1, @@ -49,8 +45,6 @@ describe('stringFormatter', () => { text: 'Unexpected foo', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; @@ -67,7 +61,6 @@ path/to/file.css const results = [ { source: 'path/to/file.css', - errored: true, warnings: [ { line: 1, @@ -77,8 +70,6 @@ path/to/file.css text: 'Unexpected foo (rule-name)', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; @@ -97,7 +88,6 @@ path/to/file.css const results = [ { source: 'path/to/file.css', - errored: true, warnings: [ { line: 1, @@ -107,8 +97,6 @@ path/to/file.css text: 'Unexpected foo', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; @@ -121,7 +109,7 @@ path/to/file.css 1 problem (1 error, 0 warnings)`); }); - it('output warnings with more than 80 characters and `process.stdout.columns` equal 90 characters', () => { + it('outputs warnings with more than 80 characters and `process.stdout.columns` equal 90 characters', () => { // For Windows tests process.stdout.isTTY = true; process.stdout.columns = 90; @@ -129,7 +117,6 @@ path/to/file.css const results = [ { source: 'path/to/file.css', - errored: true, warnings: [ { line: 1, @@ -139,8 +126,6 @@ path/to/file.css text: 'Unexpected very very very very very very very very very very very very very long foo', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; @@ -169,7 +154,6 @@ path/to/file.css text: 'Unexpected option for baz', }, ], - errored: true, warnings: [], }, { @@ -185,7 +169,6 @@ path/to/file.css text: 'Unexpected option for baz', }, ], - errored: true, warnings: [], }, ]; @@ -203,8 +186,6 @@ Deprecation Warning: Deprecated foo See: bar`); { source: 'file.css', warnings: [], - deprecations: [], - invalidOptionWarnings: [], ignored: true, }, ]; @@ -218,7 +199,6 @@ Deprecation Warning: Deprecated foo See: bar`); const results = [ { source: 'path/to/file.css', - errored: true, warnings: [ { line: 1, @@ -228,8 +208,6 @@ Deprecation Warning: Deprecated foo See: bar`); text: '', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; @@ -241,4 +219,44 @@ path/to/file.css 1 problem (1 error, 0 warnings)`); }); + + it('outputs parse errors and warnings without rule and severity', () => { + const results = [ + { + source: 'path/to/file.css', + parseErrors: [ + { + line: 1, + column: 1, + stylelintType: 'foo-error', + text: 'Cannot parse foo', + }, + ], + warnings: [ + { + line: 3, + column: 1, + rule: 'no-bar', + severity: 'error', + text: 'Disallow bar', + }, + { + line: 2, + column: 1, + text: 'Anonymous error', + }, + ], + }, + ]; + + const output = prepareFormatterOutput(results, stringFormatter); + + expect(output).toBe(stripIndent` +path/to/file.css + 1:1 × Cannot parse foo foo-error + 2:1 × Anonymous error + 3:1 × Disallow bar no-bar + +3 problems (3 errors, 0 warnings)`); + }); }); diff --git a/lib/formatters/__tests__/tapFormatter.test.js b/lib/formatters/__tests__/tapFormatter.test.js index a2d93f1b89..f9299a85bf 100644 --- a/lib/formatters/__tests__/tapFormatter.test.js +++ b/lib/formatters/__tests__/tapFormatter.test.js @@ -1,5 +1,7 @@ 'use strict'; +const { stripIndent } = require('common-tags'); + const prepareFormatterOutput = require('./prepareFormatterOutput'); const tapFormatter = require('../tapFormatter'); @@ -23,8 +25,6 @@ describe('tapFormatter', () => { source: 'path/to/file.css', errored: false, warnings: [], - deprecations: [], - invalidOptionWarnings: [], }, ]; @@ -64,15 +64,12 @@ ok 1 - path/to/file.css text: 'Unexpected foo 2', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; const output = prepareFormatterOutput(results, tapFormatter); - expect(output).toBe( - ` + expect(output).toBe(stripIndent` TAP version 13 1..1 not ok 1 - path/to/file.css @@ -94,9 +91,7 @@ messages: endLine: 11 endColumn: 2 ruleId: bar2 ---- -`.trim(), - ); +---`); }); it('outputs warnings without stdout `TTY`', () => { @@ -117,15 +112,12 @@ messages: text: 'Unexpected foo', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; const output = prepareFormatterOutput(results, tapFormatter); - expect(output).toBe( - ` + expect(output).toBe(stripIndent` TAP version 13 1..1 not ok 1 - path/to/file.css @@ -139,9 +131,7 @@ messages: endLine: 2 endColumn: 3 ruleId: bar ---- -`.trim(), - ); +---`); }); it('output warnings with more than 80 characters and `process.stdout.columns` equal 90 characters', () => { @@ -164,15 +154,12 @@ messages: text: 'Unexpected very very very very very very very very very very very very very long foo', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; const output = prepareFormatterOutput(results, tapFormatter); - expect(output).toBe( - ` + expect(output).toBe(stripIndent` TAP version 13 1..1 not ok 1 - path/to/file.css @@ -186,9 +173,7 @@ messages: endLine: 2 endColumn: 3 ruleId: bar-very-very-very-very-very-long ---- -`.trim(), - ); +---`); }); it('handles ignored file', () => { @@ -196,20 +181,60 @@ messages: { source: 'file.css', warnings: [], - deprecations: [], - invalidOptionWarnings: [], ignored: true, }, ]; const output = prepareFormatterOutput(results, tapFormatter); - expect(output).toBe( - ` + expect(output).toBe(stripIndent` TAP version 13 1..${results.length} -ok 1 - ignored ${results[0].source} -`.trim(), - ); +ok 1 - ignored ${results[0].source}`); + }); + + it('outputs parse errors and warnings without rule and severity', () => { + const results = [ + { + source: 'path/to/file.css', + errored: true, + warnings: [ + { + line: 2, + column: 1, + text: 'Unexpected foo', + }, + ], + parseErrors: [ + { + line: 1, + column: 1, + stylelintType: 'foo-error', + text: 'Cannot parse foo', + }, + ], + }, + ]; + + const output = prepareFormatterOutput(results, tapFormatter); + + expect(output).toBe(stripIndent` +TAP version 13 +1..1 +not ok 1 - path/to/file.css +--- +messages: + - message: "Cannot parse foo (foo-error)" + severity: error + data: + line: 1 + column: 1 + ruleId: foo-error + - message: "Unexpected foo" + severity: error + data: + line: 2 + column: 1 +---`); }); }); diff --git a/lib/formatters/__tests__/unixFormatter.test.js b/lib/formatters/__tests__/unixFormatter.test.js index 44f663079d..e56492197c 100644 --- a/lib/formatters/__tests__/unixFormatter.test.js +++ b/lib/formatters/__tests__/unixFormatter.test.js @@ -1,5 +1,7 @@ 'use strict'; +const { stripIndent } = require('common-tags'); + const prepareFormatterOutput = require('./prepareFormatterOutput'); const unixFormatter = require('../unixFormatter'); @@ -37,7 +39,6 @@ describe('unixFormatter', () => { const results = [ { source: 'path/to/file.css', - errored: true, warnings: [ { line: 1, @@ -50,24 +51,20 @@ describe('unixFormatter', () => { line: 10, column: 1, rule: 'bar2', - severity: 'error', + severity: 'warning', text: 'Unexpected foo 2', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; const output = prepareFormatterOutput(results, unixFormatter); - expect(output).toBe( - ` + expect(output).toBe(stripIndent` path/to/file.css:1:1: Unexpected foo [error] -path/to/file.css:10:1: Unexpected foo 2 [error] +path/to/file.css:10:1: Unexpected foo 2 [warning] -2 problems`.trimStart(), - ); +2 problems (1 error, 1 warning)`); }); it('outputs warnings without stdout `TTY`', () => { @@ -76,7 +73,6 @@ path/to/file.css:10:1: Unexpected foo 2 [error] const results = [ { source: 'path/to/file.css', - errored: true, warnings: [ { line: 1, @@ -86,22 +82,18 @@ path/to/file.css:10:1: Unexpected foo 2 [error] text: 'Unexpected foo', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; const output = prepareFormatterOutput(results, unixFormatter); - expect(output).toBe( - ` + expect(output).toBe(stripIndent` path/to/file.css:1:1: Unexpected foo [error] -1 problem`.trimStart(), - ); +1 problem (1 error, 0 warnings)`); }); - it('output warnings with more than 80 characters and `process.stdout.columns` equal 90 characters', () => { + it('outputs warnings with more than 80 characters and `process.stdout.columns` equal 90 characters', () => { // For Windows tests process.stdout.isTTY = true; process.stdout.columns = 90; @@ -109,7 +101,6 @@ path/to/file.css:1:1: Unexpected foo [error] const results = [ { source: 'path/to/file.css', - errored: true, warnings: [ { line: 1, @@ -119,19 +110,15 @@ path/to/file.css:1:1: Unexpected foo [error] text: 'Unexpected very very very very very very very very very very very very very long foo', }, ], - deprecations: [], - invalidOptionWarnings: [], }, ]; const output = prepareFormatterOutput(results, unixFormatter); - expect(output).toBe( - ` + expect(output).toBe(stripIndent` path/to/file.css:1:1: Unexpected very very very very very very very very very very very very very long foo [error] -1 problem`.trimStart(), - ); +1 problem (1 error, 0 warnings)`); }); it('handles ignored file', () => { @@ -139,8 +126,6 @@ path/to/file.css:1:1: Unexpected very very very very very very very very very ve { source: 'file.css', warnings: [], - deprecations: [], - invalidOptionWarnings: [], ignored: true, }, ]; @@ -149,4 +134,35 @@ path/to/file.css:1:1: Unexpected very very very very very very very very very ve expect(output).toBe(''); }); + + it('outputs parse errors and warnings without rule and severity', () => { + const results = [ + { + source: 'path/to/file.css', + parseErrors: [ + { + line: 1, + column: 1, + stylelintType: 'foo-error', + text: 'Cannot parse foo', + }, + ], + warnings: [ + { + line: 2, + column: 1, + text: 'Anonymous error', + }, + ], + }, + ]; + + const output = prepareFormatterOutput(results, unixFormatter); + + expect(output).toBe(stripIndent` +path/to/file.css:1:1: Cannot parse foo (foo-error) [error] +path/to/file.css:2:1: Anonymous error [error] + +2 problems (2 errors, 0 warnings)`); + }); }); diff --git a/lib/formatters/calcSeverityCounts.js b/lib/formatters/calcSeverityCounts.js new file mode 100644 index 0000000000..b19e9c66f9 --- /dev/null +++ b/lib/formatters/calcSeverityCounts.js @@ -0,0 +1,21 @@ +'use strict'; + +/** + * @typedef {import('stylelint').Severity} Severity + * + * @param {Severity} severity + * @param {Record} counts + * @returns {void} + */ +module.exports = function calcSeverityCounts(severity, counts) { + switch (severity) { + case 'error': + counts.error += 1; + break; + case 'warning': + counts.warning += 1; + break; + default: + throw new Error(`Unknown severity: "${severity}"`); + } +}; diff --git a/lib/formatters/compactFormatter.js b/lib/formatters/compactFormatter.js index 609dbbdd9b..a4318ea189 100644 --- a/lib/formatters/compactFormatter.js +++ b/lib/formatters/compactFormatter.js @@ -1,19 +1,23 @@ 'use strict'; +const preprocessWarnings = require('./preprocessWarnings'); + /** * @type {import('stylelint').Formatter} */ module.exports = function compactFormatter(results) { return results - .flatMap((result) => - result.warnings.map( + .flatMap((result) => { + const { warnings } = preprocessWarnings(result); + + return warnings.map( (warning) => `${result.source}: ` + `line ${warning.line}, ` + `col ${warning.column}, ` + `${warning.severity} - ` + `${warning.text}`, - ), - ) + ); + }) .join('\n'); }; diff --git a/lib/formatters/githubFormatter.js b/lib/formatters/githubFormatter.js index 2a47d824ec..51609fb057 100644 --- a/lib/formatters/githubFormatter.js +++ b/lib/formatters/githubFormatter.js @@ -1,5 +1,7 @@ 'use strict'; +const preprocessWarnings = require('./preprocessWarnings'); + /** * @see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions * @@ -10,15 +12,17 @@ module.exports = function githubFormatter(results, returnValue) { const metadata = returnValue.ruleMetadata; return results - .flatMap(({ source, warnings }) => - warnings.map(({ line, column, endLine, endColumn, text, severity, rule }) => { + .flatMap((result) => { + const { source, warnings } = preprocessWarnings(result); + + return warnings.map(({ line, column, endLine, endColumn, text, severity, rule }) => { const msg = buildMessage(text, metadata[rule]); return endLine === undefined ? `::${severity} file=${source},line=${line},col=${column},title=${title}::${msg}` : `::${severity} file=${source},line=${line},col=${column},endLine=${endLine},endColumn=${endColumn},title=${title}::${msg}`; - }), - ) + }); + }) .join('\n'); }; diff --git a/lib/formatters/preprocessWarnings.js b/lib/formatters/preprocessWarnings.js new file mode 100644 index 0000000000..4bba00841f --- /dev/null +++ b/lib/formatters/preprocessWarnings.js @@ -0,0 +1,74 @@ +'use strict'; + +/** @typedef {import('stylelint').LintResult} LintResult */ +/** @typedef {LintResult['parseErrors'][0]} ParseError */ +/** @typedef {LintResult['warnings'][0]} Warning */ +/** @typedef {Warning['severity']} Severity */ + +/** + * Preprocess warnings in a given lint result. + * Note that this function has a side-effect. + * + * @param {LintResult} result + * @returns {LintResult} + */ +module.exports = function preprocessWarnings(result) { + for (const error of result.parseErrors || []) { + result.warnings.push(parseErrorToWarning(error)); + } + + for (const warning of result.warnings) { + warning.severity = normalizeSeverity(warning); + } + + result.warnings.sort(byLocationOrder); + + return result; +}; + +/** + * @param {ParseError} error + * @returns {Warning} + */ +function parseErrorToWarning(error) { + return { + line: error.line, + column: error.column, + rule: error.stylelintType, + severity: 'error', + text: `${error.text} (${error.stylelintType})`, + }; +} + +/** + * @param {Warning} warning + * @returns {Severity} + */ +function normalizeSeverity(warning) { + // NOTE: Plugins may add a warning without severity, for example, + // by directly using the PostCSS `Result#warn()` API. + return warning.severity || 'error'; +} + +/** + * @param {Warning} a + * @param {Warning} b + * @returns {number} + */ +function byLocationOrder(a, b) { + // positionless first + if (!a.line && b.line) return -1; + + // positionless first + if (a.line && !b.line) return 1; + + if (a.line < b.line) return -1; + + if (a.line > b.line) return 1; + + if (a.column < b.column) return -1; + + if (a.column > b.column) return 1; + + return 0; +} diff --git a/lib/formatters/stringFormatter.js b/lib/formatters/stringFormatter.js index b7c32c8743..7a31bee928 100644 --- a/lib/formatters/stringFormatter.js +++ b/lib/formatters/stringFormatter.js @@ -5,8 +5,10 @@ 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; @@ -38,7 +40,7 @@ const symbols = { * @returns {string} */ function deprecationsFormatter(results) { - const allDeprecationWarnings = results.flatMap((result) => result.deprecations); + const allDeprecationWarnings = results.flatMap((result) => result.deprecations || []); if (allDeprecationWarnings.length === 0) { return ''; @@ -69,7 +71,7 @@ function deprecationsFormatter(results) { */ function invalidOptionsFormatter(results) { const allInvalidOptionWarnings = results.flatMap((result) => - result.invalidOptionWarnings.map((warning) => warning.text), + (result.invalidOptionWarnings || []).map((warning) => warning.text), ); const uniqueInvalidOptionWarnings = [...new Set(allInvalidOptionWarnings)]; @@ -127,25 +129,7 @@ function getMessageWidth(columnWidths) { * @return {string} */ function formatter(messages, source, cwd) { - if (!messages.length) return ''; - - const orderedMessages = [...messages].sort((a, b) => { - // positionless first - if (!a.line && b.line) return -1; - - // positionless first - if (a.line && !b.line) return 1; - - if (a.line < b.line) return -1; - - if (a.line > b.line) return 1; - - if (a.column < b.column) return -1; - - if (a.column > b.column) return 1; - - return 0; - }); + if (messages.length === 0) return ''; /** * Create a list of column widths, needed to calculate @@ -197,7 +181,7 @@ function formatter(messages, source, cwd) { return result; } - const cleanedMessages = orderedMessages.map((message) => { + const cleanedMessages = messages.map((message) => { const { line, column, severity } = message; /** * @type {[string, string, string, string, string]} @@ -232,13 +216,7 @@ function formatter(messages, source, cwd) { drawHorizontalLine: () => false, }) .split('\n') - .map( - /** - * @param {string} el - * @returns {string} - */ - (el) => el.replace(/(\d+)\s+(\d+)/, (_m, p1, p2) => dim(`${p1}:${p2}`)), - ) + .map((el) => el.replace(/(\d+)\s+(\d+)/, (_m, p1, p2) => dim(`${p1}:${p2}`)).trimEnd()) .join('\n'); return output; @@ -252,23 +230,10 @@ module.exports = function stringFormatter(results, returnValue) { output += deprecationsFormatter(results); - let errorCount = 0; - let warningCount = 0; + const counts = { error: 0, warning: 0 }; output = results.reduce((accum, result) => { - // Treat parseErrors as warnings - if (result.parseErrors) { - for (const error of result.parseErrors) { - result.warnings.push({ - line: error.line, - column: error.column, - rule: error.stylelintType, - severity: 'error', - text: `${error.text} (${error.stylelintType})`, - }); - errorCount += 1; - } - } + preprocessWarnings(result); accum += formatter( result.warnings, @@ -277,16 +242,7 @@ module.exports = function stringFormatter(results, returnValue) { ); for (const warning of result.warnings) { - switch (warning.severity) { - case 'error': - errorCount += 1; - break; - case 'warning': - warningCount += 1; - break; - default: - throw new Error(`Unknown severity: "${warning.severity}"`); - } + calcSeverityCounts(warning.severity, counts); } return accum; @@ -298,6 +254,8 @@ module.exports = function stringFormatter(results, returnValue) { if (output !== '') { output = `\n${output}\n\n`; + const errorCount = counts.error; + const warningCount = counts.warning; const total = errorCount + warningCount; if (total > 0) { diff --git a/lib/formatters/tapFormatter.js b/lib/formatters/tapFormatter.js index 1ef868ca58..e8f597df0f 100644 --- a/lib/formatters/tapFormatter.js +++ b/lib/formatters/tapFormatter.js @@ -1,5 +1,7 @@ 'use strict'; +const preprocessWarnings = require('./preprocessWarnings'); + /** * @type {import('stylelint').Formatter} */ @@ -7,6 +9,8 @@ module.exports = function tapFormatter(results) { const lines = [`TAP version 13\n1..${results.length}`]; for (const [index, result] of results.entries()) { + preprocessWarnings(result); + lines.push( `${result.errored ? 'not ok' : 'ok'} ${index + 1} - ${result.ignored ? 'ignored ' : ''}${ result.source @@ -23,10 +27,19 @@ module.exports = function tapFormatter(results) { ` data:`, ` line: ${warning.line}`, ` column: ${warning.column}`, - ` endLine: ${warning.endLine}`, - ` endColumn: ${warning.endColumn}`, - ` ruleId: ${warning.rule}`, ); + + if (typeof warning.endLine === 'number') { + lines.push(` endLine: ${warning.endLine}`); + } + + if (typeof warning.endColumn === 'number') { + lines.push(` endColumn: ${warning.endColumn}`); + } + + if (typeof warning.rule === 'string') { + lines.push(` ruleId: ${warning.rule}`); + } } lines.push('---'); diff --git a/lib/formatters/terminalLink.js b/lib/formatters/terminalLink.js index b9f647d652..3e551e804e 100644 --- a/lib/formatters/terminalLink.js +++ b/lib/formatters/terminalLink.js @@ -1,3 +1,5 @@ +'use strict'; + const supportsHyperlinks = require('supports-hyperlinks'); // ANSI escapes diff --git a/lib/formatters/unixFormatter.js b/lib/formatters/unixFormatter.js index 9db80e0dea..170ba1004c 100644 --- a/lib/formatters/unixFormatter.js +++ b/lib/formatters/unixFormatter.js @@ -1,21 +1,33 @@ 'use strict'; +const calcSeverityCounts = require('./calcSeverityCounts'); +const pluralize = require('../utils/pluralize'); +const preprocessWarnings = require('./preprocessWarnings'); + /** * @type {import('stylelint').Formatter} */ module.exports = function unixFormatter(results) { - const lines = results.flatMap((result) => - result.warnings.map( - (warning) => + const counts = { error: 0, warning: 0 }; + const lines = results.flatMap((result) => { + preprocessWarnings(result); + + return result.warnings.map((warning) => { + calcSeverityCounts(warning.severity, counts); + + return ( `${result.source}:${warning.line}:${warning.column}: ` + - `${warning.text} [${warning.severity}]\n`, - ), - ); + `${warning.text} [${warning.severity}]` + ); + }); + }); const total = lines.length; - let output = lines.join(''); + let output = lines.join('\n'); if (total > 0) { - output += `\n${total} problem${total !== 1 ? 's' : ''}\n`; + output += `\n\n${total} ${pluralize('problem', total)}`; + output += ` (${counts.error} ${pluralize('error', counts.error)}`; + output += `, ${counts.warning} ${pluralize('warning', counts.warning)})\n`; } return output;