diff --git a/packages/utils/src/lib/report-to-md.ts b/packages/utils/src/lib/report-to-md.ts index ed19c4a74..ee2329645 100644 --- a/packages/utils/src/lib/report-to-md.ts +++ b/packages/utils/src/lib/report-to-md.ts @@ -21,6 +21,7 @@ import { import { FOOTER_PREFIX, README_LINK, + compareIssues, countCategoryAudits, detailsTableHeaders, formatReportScore, @@ -216,7 +217,7 @@ function reportToAuditsSection(report: ScoredReport): string { const detailsTableData = [ detailsTableHeaders, - ...audit.details.issues.map((issue: Issue) => { + ...audit.details.issues.sort(compareIssues).map((issue: Issue) => { const severity = `${getSeverityIcon(issue.severity)} ${ issue.severity }`; diff --git a/packages/utils/src/lib/report.ts b/packages/utils/src/lib/report.ts index 361a64ae4..d615a0acc 100644 --- a/packages/utils/src/lib/report.ts +++ b/packages/utils/src/lib/report.ts @@ -4,6 +4,7 @@ import { CategoryRef, IssueSeverity as CliIssueSeverity, Format, + Issue, PersistConfig, Report, reportSchema, @@ -278,3 +279,38 @@ export function getPluginNameFromSlug( plugins.find(({ slug: pluginSlug }) => pluginSlug === slug)?.title || slug ); } + +export function compareIssues(a: Issue, b: Issue): number { + if (a.severity !== b.severity) { + return -compareIssueSeverity(a.severity, b.severity); + } + + if (!a.source && b.source) { + return -1; + } + + if (a.source && !b.source) { + return 1; + } + + if (a.source?.file !== b.source?.file) { + return a.source?.file.localeCompare(b.source?.file || '') || 0; + } + + if (!a.source?.position && b.source?.position) { + return -1; + } + + if (a.source?.position && !b.source?.position) { + return 1; + } + + if (a.source?.position?.startLine !== b.source?.position?.startLine) { + return ( + (a.source?.position?.startLine || 0) - + (b.source?.position?.startLine || 0) + ); + } + + return 0; +} diff --git a/packages/utils/src/lib/report.unit.test.ts b/packages/utils/src/lib/report.unit.test.ts index c41223f77..eae215bb3 100644 --- a/packages/utils/src/lib/report.unit.test.ts +++ b/packages/utils/src/lib/report.unit.test.ts @@ -1,10 +1,16 @@ import { vol } from 'memfs'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { CategoryRef, IssueSeverity, PluginReport } from '@code-pushup/models'; +import { + CategoryRef, + Issue, + IssueSeverity, + PluginReport, +} from '@code-pushup/models'; import { MEMFS_VOLUME, report } from '@code-pushup/models/testing'; import { calcDuration, compareIssueSeverity, + compareIssues, countWeightedRefs, getPluginNameFromSlug, loadReport, @@ -223,3 +229,63 @@ describe('getPluginNameFromSlug', () => { expect(getPluginNameFromSlug('plugin-b', plugins)).toBe('Plugin B'); }); }); + +describe('sortAuditIssues', () => { + it('should sort issues by severity and source file', () => { + const mockIssues = [ + { severity: 'warning', source: { file: 'b' } }, + { severity: 'error', source: { file: 'c' } }, + { severity: 'error', source: { file: 'a' } }, + { severity: 'info', source: { file: 'b' } }, + ] as Issue[]; + const sortedIssues = [...mockIssues].sort(compareIssues); + expect(sortedIssues).toEqual([ + { severity: 'error', source: { file: 'a' } }, + { severity: 'error', source: { file: 'c' } }, + { severity: 'warning', source: { file: 'b' } }, + { severity: 'info', source: { file: 'b' } }, + ]); + }); + + it('should sort issues by source file and source start line', () => { + const mockIssues = [ + { severity: 'info', source: { file: 'b', position: { startLine: 2 } } }, + { severity: 'info', source: { file: 'c', position: { startLine: 1 } } }, + { severity: 'info', source: { file: 'a', position: { startLine: 2 } } }, + { severity: 'info', source: { file: 'b', position: { startLine: 1 } } }, + ] as Issue[]; + const sortedIssues = [...mockIssues].sort(compareIssues); + expect(sortedIssues).toEqual([ + { severity: 'info', source: { file: 'a', position: { startLine: 2 } } }, + { severity: 'info', source: { file: 'b', position: { startLine: 1 } } }, + { severity: 'info', source: { file: 'b', position: { startLine: 2 } } }, + { severity: 'info', source: { file: 'c', position: { startLine: 1 } } }, + ]); + }); + + it('should sort issues without source on top of same severity', () => { + const mockIssues = [ + { severity: 'info', source: { file: 'b', position: { startLine: 2 } } }, + { severity: 'info', source: { file: 'c', position: { startLine: 1 } } }, + { + severity: 'warning', + source: { file: 'a', position: { startLine: 2 } }, + }, + { severity: 'info', source: { file: 'b', position: { startLine: 1 } } }, + { severity: 'info', source: { file: 'b' } }, + { severity: 'error' }, + ] as Issue[]; + const sortedIssues = [...mockIssues].sort(compareIssues); + expect(sortedIssues).toEqual([ + { severity: 'error' }, + { + severity: 'warning', + source: { file: 'a', position: { startLine: 2 } }, + }, + { severity: 'info', source: { file: 'b' } }, + { severity: 'info', source: { file: 'b', position: { startLine: 1 } } }, + { severity: 'info', source: { file: 'b', position: { startLine: 2 } } }, + { severity: 'info', source: { file: 'c', position: { startLine: 1 } } }, + ]); + }); +});