uui-build/linting/formatters/formatterUtils.js (243 lines of code) (raw):
const path = require('path');
const { logger } = require('../../utils/loggerUtils');
const SUM_TOP_N = 300;
const ROOT_DIR = path.resolve(`${__dirname}/../../../.`);
module.exports = {
ROOT_DIR,
getReportLocationPath,
sumComparator,
convertSummaryToHtml,
createSummaryFromMessages,
convertResultsToHtml,
reportUnnecessaryRulesToBeFixed,
logSummary,
};
function getReportLocationPath() {
const i = process.argv.indexOf('-o');
if (i !== -1) {
const dir = process.argv[i + 1];
return forwardSlashes(path.resolve(ROOT_DIR, dir));
}
}
function forwardSlashes(pathStr) {
return pathStr.replace(/\\/g, '/');
}
function getSeverityOpts(kind) {
let error;
let warning;
if (kind === 'eslint') {
warning = {
id: 1,
label: 'Warning',
key: 'warning',
};
error = {
id: 2,
label: 'Error',
key: 'error',
};
} else {
error = {
id: 'error',
label: 'Error',
key: 'error',
};
warning = {
id: 'warning',
label: 'Warning',
key: 'warning',
};
}
return {
warning,
error,
};
}
/**
* Builds HTML table with statistics about amount of errors per severity/rule.
* "sum" is the result returned from "createSummaryFromMessages" method.
*
* @returns {string} html string
*/
function convertSummaryToHtml({ summary, refRuleTemplate, kind = 'eslint' }) {
const SEV_OPTS = getSeverityOpts(kind);
function getSumRows(s, severityOpt) {
const rulesTopN = Object.keys(summary[severityOpt.id].amountByRuleId).slice(0, SUM_TOP_N);
return rulesTopN.map((rule) => {
const googleSearch = refRuleTemplate(rule);
const url = `https://www.google.com/search?q=${encodeURIComponent(googleSearch)}`;
const amount = summary[severityOpt.id].amountByRuleId[rule];
return `<tr><td><a target=”_blank” href="${url}">${rule}</a></td>
<td>${amount}</td></tr>`;
});
}
function getSumFormatted({ severityOpt }) {
const label = severityOpt.label;
const summaryRows = getSumRows(summary, severityOpt).join('');
return `
<h3 style="color: #b94a48;">${label}s by rule (Top ${SUM_TOP_N})</h3>
<table style="width: 400px; max-width: 100%;">
<thead><tr><th>rule</th><th>amount</th></tr></thead>
<tbody>${summaryRows}</tbody>
</table>
`;
}
const totalErr = summary[SEV_OPTS.error.id].amount;
const totalWarn = summary[SEV_OPTS.warning.id].amount;
return `
<div style="margin-left: 30px;">
<h3 style="color: #b94a48;">Total ${totalErr + totalWarn} problems (${totalErr} errors, ${totalWarn} warnings). Generated: ${new Date()}</h3>
${getSumFormatted({ severityOpt: SEV_OPTS.error })}
${getSumFormatted({ severityOpt: SEV_OPTS.warning })}
</div>
`;
}
/**
* @param obj
*/
function sortKeysByValue(obj) {
const keys = Object.keys(obj);
keys.sort((k1, k2) => {
return obj[k2] - obj[k1];
});
return keys.reduce((acc, k) => {
acc[k] = obj[k];
return acc;
}, {});
}
/**
* @returns {{"<severity>": {amount: number, amountByRuleId: {}}, "<severity>": {amount: number, amountByRuleId: {}}}}
*/
function createSummaryFromMessages(messages, initial, ruleIdPropName = 'ruleId', kind = 'eslint') {
const SEV_OPTS = getSeverityOpts(kind);
const sum = initial || {
[SEV_OPTS.error.id]: {
amount: 0,
amountByRuleId: {},
},
[SEV_OPTS.warning.id]: {
amount: 0,
amountByRuleId: {},
},
};
messages.forEach((msg) => {
const severity = msg.severity;
const ruleId = msg[ruleIdPropName];
if (!sum[severity].amountByRuleId[ruleId]) {
sum[severity].amountByRuleId[ruleId] = 0;
}
sum[severity].amount += 1;
sum[severity].amountByRuleId[ruleId] += 1;
});
Object.keys(sum).forEach((k) => {
sum[k].amountByRuleId = sortKeysByValue(sum[k].amountByRuleId);
});
return sum;
}
/**
* s1 and s2 params look like this:
* { 1: { amount: 100 }, 2: { amount: 200 } }
* @returns {number}
*/
function sumComparator(s1, s2, kind = 'eslint') {
const SEV_OPTS = getSeverityOpts(kind);
return s2[SEV_OPTS.error.id].amount - s1[SEV_OPTS.error.id].amount || s2[SEV_OPTS.warning.id].amount - s1[SEV_OPTS.warning.id].amount;
}
function convertResultsToHtml(results, kind = 'eslint') {
return `
<style>
.m-expand-all .c-file-message {
display: table-row !important;
}
.c-file-path td {
padding: 5px 10px;
font-weight: 400;
font-size: medium;
}
.c-messages-by-file {
border-collapse: collapse;
width: 1000px; max-width: 100%;
}
.c-messages-by-file th {
text-align: left;
padding: 20px;
}
.c-messages-by-file,
.c-messages-by-file td,
.c-messages-by-file th,
.c-messages-by-file tr
{ border: 1px solid gray; }
.c-file-message {
color: #6C6E7A;
display: none;
}
.severity-label-error { color: #A72014; }
.severity-label-warning { color: #D67B0B; }
</style>
<script>
function toggleExpandAll() {
if (document.getElementById('expand-all-toggler').checked) {
document.body.classList.add('m-expand-all')
} else {
document.body.classList.remove('m-expand-all')
}
}
</script>
<table class="c-messages-by-file">
<thead>
<tr><th>Show details: <input id="expand-all-toggler" type="checkbox" onchange="toggleExpandAll()" /></th><th></th></tr>
<tr><th>File</th><th>Amount</th></tr>
</thead>
<tbody>
${results.map((r) => singleFileResultsToHtml(r, kind)).join('')}
</tbody>
</table>
`;
}
function singleFileResultsToHtml(fileRow, kind) {
const p = forwardSlashes(path.relative(ROOT_DIR, fileRow[kind === 'eslint' ? 'filePath' : 'source']));
const SEV_OPTS = getSeverityOpts(kind);
const totalErr = fileRow.fileSummary[SEV_OPTS.error.id].amount;
const totalWarn = fileRow.fileSummary[SEV_OPTS.warning.id].amount;
const total = `${totalErr} errors, ${totalWarn} warnings`;
if (totalErr + totalWarn === 0) {
return '';
}
function htmlEsc(t) {
return t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
.replace(/'/g, ''');
}
function msgToHtml(msg) {
const row = msg.line;
const col = msg.column;
const rule = msg[kind === 'eslint' ? 'ruleId' : 'rule'];
const severity = msg.severity;
const text = htmlEsc(msg[kind === 'eslint' ? 'message' : 'text']);
const s = ' ';
const sevObj = Object.values(SEV_OPTS).find(({ id }) => id === severity);
const severityLabel = sevObj.label;
return `
<tr class="c-file-message">
<td colspan="2">
${row}:${col}${s}<span class="severity-label-${sevObj.key}">${severityLabel}</span>${s}${rule}${s}${text}
</td>
</tr>
`;
}
return `
<tr class="c-file-path">
<td>${p}</td><td>${total}</td>
</tr>
${fileRow[kind === 'eslint' ? 'messages' : 'warnings'].map(msgToHtml).join('')}
`;
}
function reportUnnecessaryRulesToBeFixed(summary, rulesToBeFixed) {
const mapOfRulesWithIssues = Object.values(summary).reduce((acc, map) => {
return { ...acc, ...map.amountByRuleId };
}, {});
const unnecessaryOnes = rulesToBeFixed.filter((r) => {
return !mapOfRulesWithIssues[r];
});
if (unnecessaryOnes.length > 0) {
logger.warn(`There are no issues reported for next rules. Please consider removing them from rulesToBeFixed.js\n${unnecessaryOnes.join(', ')}`);
}
}
function logSummary(summary, kind = 'eslint') {
const SEV_OPTS = getSeverityOpts(kind);
const sum = {
errors: summary[SEV_OPTS.error.id]?.amount,
warnings: summary[SEV_OPTS.warning.id]?.amount,
};
logger.table({ caption: 'Summary', data: sum });
if (sum.errors) {
const detailsErr = summary[SEV_OPTS.error.id]?.amountByRuleId;
logger.table({ caption: 'Errors', data: detailsErr });
}
if (sum.warnings) {
const detailsWarn = summary[SEV_OPTS.warning.id]?.amountByRuleId;
logger.table({ caption: 'Warnings', data: detailsWarn });
}
}