diff --git a/packages/validator/src/cli-validator/utils/index.js b/packages/validator/src/cli-validator/utils/index.js index 075b26353..efe2038cd 100644 --- a/packages/validator/src/cli-validator/utils/index.js +++ b/packages/validator/src/cli-validator/utils/index.js @@ -1,5 +1,5 @@ /** - * Copyright 2023 IBM Corporation. + * Copyright 2023 - 2025 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ @@ -8,6 +8,7 @@ module.exports = { createCLIOptions: require('./cli-options'), getCopyrightString: require('./get-copyright-string'), getDefaultRulesetVersion: require('./get-default-ruleset-version'), + parseViolationMessage: require('./parse-violation-message'), getLocalRulesetVersion: require('./get-local-ruleset-version'), getVersionString: require('./get-version-string'), preprocessFile: require('./preprocess-file'), diff --git a/packages/validator/src/cli-validator/utils/parse-violation-message.js b/packages/validator/src/cli-validator/utils/parse-violation-message.js new file mode 100644 index 000000000..9f6cbc3aa --- /dev/null +++ b/packages/validator/src/cli-validator/utils/parse-violation-message.js @@ -0,0 +1,15 @@ +/** + * Copyright 2025 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +// Rule violations messages, use a standard format of +// ": ". This function +// provides a quick utility to extract the generalized and +// detail sections from the message. +function parseViolationMessage(message) { + const [general, detail] = message.split(':'); + return [general.trim(), detail?.trim()]; +} + +module.exports = parseViolationMessage; diff --git a/packages/validator/src/markdown-report/tables/rule-violation-details.js b/packages/validator/src/markdown-report/tables/rule-violation-details.js index f73051dfa..e1a5b84b2 100644 --- a/packages/validator/src/markdown-report/tables/rule-violation-details.js +++ b/packages/validator/src/markdown-report/tables/rule-violation-details.js @@ -1,28 +1,68 @@ /** - * Copyright 2024 IBM Corporation. + * Copyright 2024 - 2025 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ +const { parseViolationMessage } = require('../../cli-validator/utils'); + const MarkdownTable = require('../markdown-table'); -function getTable({ error, warning }) { - const table = new MarkdownTable( - 'Rule', - 'Message', - 'Path', - 'Line', - 'Severity' - ); +function getTables(violations) { + // Stores the header and the table for each rule. + const ruleReports = {}; + + for (const severity of ['error', 'warning']) { + for (const { message, path, rule, line } of violations[severity].results) { + const [generalizedMessage, details] = parseViolationMessage(message); + + // Add a new entry for this rule. + if (!ruleReports[rule]) { + ruleReports[rule] = { + header: createHeader(rule, severity, generalizedMessage), + }; - error.results.forEach(({ message, path, rule, line }) => { - table.addRow(rule, message, path.join('.'), line, 'error'); - }); + // Add an extra column if the rule includes details in the message. + if (details) { + ruleReports[rule].table = new MarkdownTable( + 'Line', + 'Path', + 'Details' + ); + } else { + ruleReports[rule].table = new MarkdownTable('Line', 'Path'); + } + } - warning.results.forEach(({ message, path, rule, line }) => { - table.addRow(rule, message, path.join('.'), line, 'warning'); - }); + // Add additional rows to the table for the rule. + if (details) { + ruleReports[rule].table.addRow(line, path.join('.'), details); + } else { + ruleReports[rule].table.addRow(line, path.join('.')); + } + } + } - return table.render(); + let tableOutput = ''; + for (const { header, table } of Object.values(ruleReports)) { + tableOutput += `${header}${table.render()}\n\n`; + } + + // Remove the final newline characters from the string. + return tableOutput.trim(); } -module.exports = getTable; +module.exports = getTables; + +function createHeader(ruleName, severity, generalizedMessage) { + const severityColors = { + error: '🔴', + warning: '🟠', + }; + + // Template string for header. + return `### ${severityColors[severity]} ${ruleName} + +_${generalizedMessage}_ + +`; +} diff --git a/packages/validator/src/spectral/index.js b/packages/validator/src/spectral/index.js index e10bae1a4..8901653af 100644 --- a/packages/validator/src/spectral/index.js +++ b/packages/validator/src/spectral/index.js @@ -1,5 +1,5 @@ /** - * Copyright 2017 - 2024 IBM Corporation. + * Copyright 2017 - 2025 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ @@ -14,6 +14,7 @@ const { checkRulesetVersion, getFileExtension, getLocalRulesetVersion, + parseViolationMessage, } = require('../cli-validator/utils'); const { findSpectralRuleset } = require('./utils'); @@ -91,7 +92,7 @@ function convertResults(spectralResults, { config, logger }) { }); // compute a generalized message for the summary - const genMessage = r.message.split(':')[0]; + const genMessage = parseViolationMessage(r.message)[0]; if (!summaryHelper[severity][genMessage]) { summaryHelper[severity][genMessage] = 0; } diff --git a/packages/validator/test/markdown-report/report.test.js b/packages/validator/test/markdown-report/report.test.js index 5877f713d..bc04047e9 100644 --- a/packages/validator/test/markdown-report/report.test.js +++ b/packages/validator/test/markdown-report/report.test.js @@ -1,5 +1,5 @@ /** - * Copyright 2024 IBM Corporation. + * Copyright 2024 - 2025 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ @@ -18,7 +18,7 @@ describe('getReport tests', function () { // Check all subtitle-level headers. const headers = report .split('\n') - .filter(l => l.startsWith('##')) + .filter(l => l.startsWith('## ')) .map(l => l.slice(3)); expect(headers).toEqual([ 'Quick view', diff --git a/packages/validator/test/markdown-report/tables/rule-violation-details.test.js b/packages/validator/test/markdown-report/tables/rule-violation-details.test.js index f140dc1ca..3978f4a51 100644 --- a/packages/validator/test/markdown-report/tables/rule-violation-details.test.js +++ b/packages/validator/test/markdown-report/tables/rule-violation-details.test.js @@ -1,5 +1,5 @@ /** - * Copyright 2024 IBM Corporation. + * Copyright 2024 - 2025 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ @@ -8,19 +8,39 @@ const validatorResults = require('../../test-utils/mock-json-output.json'); describe('ruleViolationDetails table tests', function () { it('should produce a table with all rule violations from the results', function () { - const tableRows = ruleViolationDetails(validatorResults).split('\n'); + // Filter out empty lines, no need to check those. + const tableRows = ruleViolationDetails(validatorResults) + .split('\n') + .filter(row => !!row); - expect(tableRows).toHaveLength(5); - expect(tableRows[0]).toBe('| Rule | Message | Path | Line | Severity |'); - expect(tableRows[1]).toBe('| --- | --- | --- | --- | --- |'); - expect(tableRows[2]).toBe( - '| ibm-no-consecutive-path-parameter-segments | Path contains two or more consecutive path parameter references: /pets/{pet_id}/{id} | paths./pets/{pet_id}/{id} | 84 | error |' + expect(tableRows).toHaveLength(15); + + expect(tableRows[0]).toBe( + '### 🔴 ibm-no-consecutive-path-parameter-segments' ); - expect(tableRows[3]).toBe( - "| ibm-integer-attributes | Integer schemas should define property 'minimum' | components.schemas.Pet.properties.id | 133 | error |" + expect(tableRows[1]).toBe( + '_Path contains two or more consecutive path parameter references_' ); + expect(tableRows[2]).toBe('| Line | Path | Details |'); + expect(tableRows[3]).toBe('| --- | --- | --- |'); expect(tableRows[4]).toBe( - "| ibm-anchored-patterns | A regular expression used in a 'pattern' attribute should be anchored with ^ and $ | components.schemas.Error.properties.message.pattern | 233 | warning |" + '| 84 | paths./pets/{pet_id}/{id} | /pets/{pet_id}/{id} |' + ); + expect(tableRows[5]).toBe('### 🔴 ibm-integer-attributes'); + expect(tableRows[6]).toBe( + "_Integer schemas should define property 'minimum'_" + ); + expect(tableRows[7]).toBe('| Line | Path |'); + expect(tableRows[8]).toBe('| --- | --- |'); + expect(tableRows[9]).toBe('| 133 | components.schemas.Pet.properties.id |'); + expect(tableRows[10]).toBe('### 🟠 ibm-anchored-patterns'); + expect(tableRows[11]).toBe( + "_A regular expression used in a 'pattern' attribute should be anchored with ^ and $_" + ); + expect(tableRows[12]).toBe('| Line | Path |'); + expect(tableRows[13]).toBe('| --- | --- |'); + expect(tableRows[14]).toBe( + '| 233 | components.schemas.Error.properties.message.pattern |' ); }); });