@ui5/linter
Version:
A static code analysis tool for UI5
281 lines (278 loc) • 9.59 kB
JavaScript
import { LintMessageSeverity } from "../linter/messages.js";
import encodeXML from "./lib/encodeXML.js";
export class Html {
format(lintResults, showDetails, version, autofix, quiet = false) {
let totalErrorCount = 0;
let totalWarningCount = 0;
let totalFatalErrorCount = 0;
// Build the HTML content
let resultsHtml = "";
lintResults.forEach(({ filePath, messages, errorCount, warningCount, fatalErrorCount }) => {
if (!errorCount && !warningCount) {
// Skip files without errors or warnings
return;
}
// Accumulate totals
totalErrorCount += errorCount;
totalWarningCount += warningCount;
totalFatalErrorCount += fatalErrorCount;
// Add the file path as a section header
resultsHtml += encodeXML `<div class="file">
<h3>${filePath}</h3>
<table>
<thead>
<tr>
<th>Severity</th>
<th>Rule</th>
<th>Location</th>
<th>Message</th>`;
resultsHtml += `${showDetails ? "<th>Details</th>" : ""}
</tr>
</thead>
<tbody>`;
// Sort messages by severity (fatal errors first, then errors, then warnings)
messages.sort((a, b) => {
// Handle fatal errors first to push them to the bottom
if (a.fatal !== b.fatal) {
return a.fatal ? -1 : 1; // Fatal errors go to the top
}
// Then, compare by severity
if (a.severity !== b.severity) {
return b.severity - a.severity;
}
// If severity is the same, compare by line number
if ((a.line ?? 0) !== (b.line ?? 0)) {
return (a.line ?? 0) - (b.line ?? 0);
}
// If both severity and line number are the same, compare by column number
return (a.column ?? 0) - (b.column ?? 0);
});
// Format each message
messages.forEach((msg) => {
const severityClass = this.getSeverityClass(msg.severity, msg.fatal);
const severityText = this.formatSeverity(msg.severity, msg.fatal);
const location = `${msg.line ?? 0}:${msg.column ?? 0}`;
resultsHtml += encodeXML `<tr class="${severityClass}">`;
resultsHtml += encodeXML `<td>${severityText}</td>`;
resultsHtml += encodeXML `<td><a href="https://github.com/UI5/linter/blob/v${version}/docs/Rules.md#${msg.ruleId}" target="_blank">${msg.ruleId}</a></td>`;
resultsHtml += encodeXML `<td><code>${location}</code></td>`;
resultsHtml += encodeXML `<td>${msg.message}</td>`;
if (showDetails && msg.messageDetails) {
resultsHtml += `<td class="details">${this.formatMessageDetails(msg)}</td>`;
}
else if (showDetails) {
resultsHtml += `<td class="details"></td>`;
}
resultsHtml += `</tr>`;
});
resultsHtml += `</tbody></table></div>`;
});
// Build summary
const totalCount = quiet ? totalErrorCount : totalErrorCount + totalWarningCount;
const errorsText = `${totalErrorCount} ${totalErrorCount === 1 ? "error" : "errors"}`;
const warningsText = quiet ? "" : `, ${totalWarningCount} ${totalWarningCount === 1 ? "warning" : "warnings"}`;
const problemsText = `${totalCount} ${totalCount === 1 ? "problem" : "problems"}`;
const summary = `<div class="summary">
<h2>Summary</h2>
<p>
${problemsText} (${errorsText}${warningsText})
</p>
${totalFatalErrorCount ? `<p><strong>${totalFatalErrorCount} fatal errors</strong></p>` : ""}
${!autofix && totalCount > 0 ?
"<p>Run <code>ui5lint --fix</code> to resolve all auto-fixable problems</p>" :
""}
</div>`;
// Full HTML document with some basic styling
const html = `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UI5 Linter Report</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.5;
max-width: 1600px;
margin: 0 auto;
padding: 20px;
color: #333;
}
h1, h2 {
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
color: #24292e;
}
h3 {
padding: 10px;
margin: 0;
background-color: #f6f8fa;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border: 1px solid #eaecef;
border-bottom: none;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
border: 1px solid #eaecef;
border-radius: 4px;
}
th, td {
text-align: left;
padding: 8px 12px;
border-bottom: 1px solid #eaecef;
}
th {
background-color: #f6f8fa;
font-weight: 600;
}
tr.error {
background-color: #fff5f5;
}
tr.error td:first-child {
color: #d73a49;
font-weight: 600;
}
tr.warning {
background-color: #fffbea;
}
tr.warning td:first-child {
color: #e36209;
font-weight: 600;
}
tr.fatal-error {
background-color: #ffdce0;
}
tr.fatal-error td:first-child {
color: #b31d28;
font-weight: 600;
}
td.details a {
overflow-wrap: anywhere;
}
code {
background-color: #f6f8fa;
padding: 0.2em 0.4em;
border-radius: 3px;
font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
}
.summary {
margin-bottom: 30px;
}
.file {
margin-bottom: 30px;
}
.note {
margin-top: 20px;
padding: 10px;
background-color: #f6f8fa;
border-radius: 4px;
font-size: 14px;
}
a {
color: #0366d6;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
body {
padding: 10px;
}
table {
display: block;
overflow-x: auto;
}
}
</style>
</head>
<body>
<h1>UI5 Linter Report</h1>
<p>Generated with UI5 Linter v${version}</p>
${summary}
${resultsHtml && `<h2>Findings</h2>${resultsHtml}`}
${!showDetails && totalCount > 0 ?
"<div class=\"note\"><strong>Note:</strong> Use <code>ui5lint --details</code> " +
"to show more information about the findings.</div>" :
""}
</body>
</html>`;
return html;
}
// Formats the severity of the lint message
formatSeverity(severity, fatal) {
if (fatal === true) {
return "Fatal Error";
}
else if (severity === LintMessageSeverity.Warning) {
return "Warning";
}
else if (severity === LintMessageSeverity.Error) {
return "Error";
}
else {
throw new Error(`Unknown severity: ${LintMessageSeverity[severity]}`);
}
}
// Returns CSS class name based on severity
getSeverityClass(severity, fatal) {
if (fatal === true) {
return "fatal-error";
}
else if (severity === LintMessageSeverity.Warning) {
return "warning";
}
else if (severity === LintMessageSeverity.Error) {
return "error";
}
else {
return "";
}
}
// Formats additional message details if available
formatMessageDetails(msg) {
if (!msg.messageDetails) {
return "";
}
// Replace multiple spaces, tabs, or newlines with a single space for clean output
// This more comprehensive regex handles all whitespace characters
const cleanedDetails = msg.messageDetails.replace(/[\s\t\r\n]+/g, " ");
// Check if the messageDetails contain any URLs
const urlRegex = /(https?:\/\/[^\s)]+)|(\([^(]*?)(https?:\/\/[^\s)]+)([^)]*?\))|(\b(?:www\.|ui5\.sap\.com)[^\s)]+)/g;
const convertURLsToLinks = (url) => {
return url.replace(urlRegex, (match, directUrl, beforePar, urlInPar, afterPar, domainUrl) => {
if (directUrl) {
// Direct URL without parentheses
return encodeXML `<a href="${directUrl}" target="_blank">${directUrl}</a>`;
}
else if (urlInPar) {
// URL inside parentheses - keep the parentheses as text but make the URL a link
return encodeXML `${beforePar}<a href="${urlInPar}" target="_blank">${urlInPar}</a>${afterPar}`;
}
else if (domainUrl) {
// Domain starting with www. or ui5.sap.com without http(s)://
const fullUrl = typeof domainUrl === "string" && domainUrl.startsWith("www.") ?
`http://${domainUrl}` :
`https://${domainUrl}`;
return encodeXML `<a href="${fullUrl}" target="_blank">${domainUrl}</a>`;
}
return match;
});
};
return cleanedDetails.replace(new RegExp(`((?:${urlRegex.source})|[^]+?)`, "g"), (m) => {
if (!urlRegex.test(m)) {
// Encode all non-URL parts XSS-safely
return encodeXML `${m}`;
}
else {
// Generate hyperlinks with XSS-safe encoding
return convertURLsToLinks(m);
}
});
}
}
//# sourceMappingURL=html.js.map