All Downloads are FREE. Search and download functionalities are using the official Maven repository.

name.remal.gradle_plugins.dsl.utils.code_quality.FindBugsReport-writeHtml.kt Maven / Gradle / Ivy

There is a newer version: 1.9.2
Show newest version
@file:Suppress("MaxLineLength")

package name.remal.gradle_plugins.dsl.utils.code_quality

import kotlinx.html.FlowOrMetaDataContent
import kotlinx.html.HTML
import kotlinx.html.body
import kotlinx.html.br
import kotlinx.html.classes
import kotlinx.html.code
import kotlinx.html.div
import kotlinx.html.h1
import kotlinx.html.head
import kotlinx.html.html
import kotlinx.html.lang
import kotlinx.html.meta
import kotlinx.html.p
import kotlinx.html.pre
import kotlinx.html.script
import kotlinx.html.stream.appendHTML
import kotlinx.html.strong
import kotlinx.html.style
import kotlinx.html.table
import kotlinx.html.tbody
import kotlinx.html.td
import kotlinx.html.title
import kotlinx.html.tr
import kotlinx.html.unsafe
import name.remal.createParentDirectories
import name.remal.default
import name.remal.forceDeleteRecursively
import name.remal.nullIf
import name.remal.nullIfEmpty
import org.jsoup.nodes.Element
import org.jsoup.parser.Parser.parseFragment
import org.jsoup.parser.Tag
import java.io.File
import java.io.OutputStream
import java.io.Writer
import java.nio.charset.StandardCharsets.UTF_8
import kotlin.collections.any
import kotlin.collections.asSequence
import kotlin.collections.emptySet
import kotlin.collections.filterKeys
import kotlin.collections.forEach
import kotlin.collections.joinToString
import kotlin.collections.mutableSetOf
import kotlin.collections.plus
import kotlin.collections.set
import kotlin.collections.toList

fun FindBugsReport.writeHtmlTo(outputStream: OutputStream, projectDir: File? = null, toolName: String? = null, rootProjectDir: File? = projectDir) = outputStream.write(asHtmlString(projectDir, toolName, rootProjectDir).toByteArray(UTF_8))
fun FindBugsReport.writeHtmlTo(writer: Writer, projectDir: File? = null, toolName: String? = null, rootProjectDir: File? = projectDir) = writer.write(asHtmlString(projectDir, toolName, rootProjectDir))
fun FindBugsReport.writeHtmlTo(file: File, projectDir: File? = null, toolName: String? = null, rootProjectDir: File? = projectDir) = file.forceDeleteRecursively().createParentDirectories().writeText(asHtmlString(projectDir, toolName, rootProjectDir), UTF_8)

@Suppress("ComplexMethod", "LongMethod")
fun FindBugsReport.asHtmlString(projectDir: File? = null, toolName: String? = null, rootProjectDir: File? = projectDir) = buildHtmlString {
    val tool = tool.nullIfEmpty() ?: toolName.nullIfEmpty()
    val version = version.nullIfEmpty()
    val title = buildString {
        if (tool != null) {
            append(tool)
            append(" analysis report")
        } else {
            append("Analysis report")
        }
    }

    head {
        meta { attributes["http-equiv"] = "Content-Type"; attributes["content"] = "text/html;charset=UTF-8" }
        meta { attributes["http-equiv"] = "X-UA-Compatible"; attributes["content"] = "IE=edge, chrome=1" }
        meta("SKYPE_TOOLBAR", "SKYPE_TOOLBAR_PARSER_COMPATIBLE")
        meta("viewport", "width=device-width, initial-scale=1, shrink-to-fit=no")

        title(title)

        FindBugsReportWebJars.resources
            .filterKeys { it.endsWith(".css") }
            .forEach { path, content ->
                cssResource(content, path)
            }
    }

    body {
        val usedLanguages = mutableSetOf()
        div("container") container@{
            h1 { +title }
            if (tool != null && version != null) {
                p {
                    text(buildString {
                        append(tool)
                        append(" version: ")
                        append(version)
                    })
                }
            }
            br

            val sortedBugs = sortedBugs
            if (sortedBugs.isEmpty()) {
                p("alert alert-success") { +"No $tool violations were found" }
                return@container
            }

            val srcDirs = projectDir?.let { projectDir ->
                project?.srcDirs.default(emptySet()).asSequence()
                    .mapNotNull { projectDir.resolve(it) }
                    .mapNotNull(File::getAbsoluteFile)
                    .distinct()
                    .toList()
            }
            val hasCategories = sortedBugs.any { !it.category.isNullOrEmpty() }
            val hasTypes = sortedBugs.any { !it.type.isNullOrEmpty() }
            table("table table-borderless") {
                sortedBugs.forEach { bug ->
                    tbody {
                        val className = bug.location?.className.nullIfEmpty()
                        val sourcePath = bug.location?.sourceFile.nullIfEmpty()
                        val startLine = bug.location?.startLine
                        val sourceFile = if (sourcePath != null && srcDirs != null) {
                            srcDirs.asSequence()
                                .map { it.resolve(sourcePath) }
                                .filter(File::isFile)
                                .mapNotNull(File::getAbsoluteFile)
                                .firstOrNull()
                        } else {
                            null
                        }
                        val relativePath = if (sourceFile != null && rootProjectDir != null) {
                            rootProjectDir.toPath().relativize(sourceFile.toPath())
                                .nullIf { isAbsolute }
                                ?.toString()
                                ?.replace(File.pathSeparatorChar, '/')
                                .nullIf { startsWith("../") }
                        } else {
                            null
                        }

                        tr("table-secondary") {
                            style = "font-size: 110%"

                            if (hasCategories) {
                                td {
                                    style = "white-space: nowrap"
                                    +bug.category.default()
                                }
                            }
                            if (hasTypes) {
                                td {
                                    style = "white-space: nowrap"
                                    +bug.type.default()
                                }
                            }
                            td {
                                attributes["width"] = "100%"
                                +buildString {
                                    if (relativePath != null) {
                                        append(relativePath)
                                        if (startLine != null) append(':').append(startLine)
                                        return@buildString
                                    }

                                    if (sourceFile != null) {
                                        append(sourceFile.path)
                                        if (startLine != null) append(':').append(startLine)
                                        return@buildString
                                    }

                                    if (className != null) {
                                        append(className)
                                        if (startLine != null) append(':').append(startLine)
                                        return@buildString
                                    }
                                }
                            }
                        }
                        tr {
                            td {
                                if (hasCategories && hasTypes) {
                                    colSpan = "3"
                                } else if (hasCategories || hasTypes) {
                                    colSpan = "2"
                                }

                                div {
                                    div {
                                        strong { +bug.message.default() }
                                    }

                                    if (bug.message != bug.type?.let(types::get)?.let(FindBugsType::textDescription)) {
                                        br
                                        div {
                                            unsafe {
                                                val sourceHtml = bug.type?.let(types::get)?.let(FindBugsType::htmlDescription).nullIfEmpty() ?: return@unsafe
                                                parseFragment("
$sourceHtml
", Element("div"), "").toList().forEach { node -> if (node is Element) { for (headerNumber in (1..6)) { val elements = node.select("h$headerNumber").toList() elements.forEach { element -> element.before(Element("br")) element.after(Element(Tag.valueOf("div"), "", element.attributes()).apply { html("${element.html()}") }) element.remove() } } } raw(node.outerHtml()) } } } } if (sourceFile != null && startLine != null) { val endLine = Math.max(bug.location?.endLine ?: startLine, startLine) val startIndex = startLine - 1 val endIndex = endLine - 1 val allLines = sourceFile.readLines(UTF_8) val lineMargin = 3 val startMarginIndex = Math.max(0, startIndex - lineMargin) val endMarginIndex = Math.min(endIndex + lineMargin + 1, allLines.size) val linesToDisplay = allLines.subList(startMarginIndex, endMarginIndex) br pre("container") { style = "max-height: 25rem" classes += "line-numbers" if (startMarginIndex > 0) { (startMarginIndex + 1).toString().let { attributes["data-start"] = it attributes["data-line-offset"] = it } } if (startLine != endLine) { attributes["data-line"] = "$startLine-$endLine" } else { attributes["data-line"] = startLine.toString() } val sourceLanguage = sourceFile.sourceLanguage if (sourceLanguage != null) { classes += "language-$sourceLanguage" usedLanguages.add(sourceLanguage) } code { +linesToDisplay.joinToString("\n") } } } br br } } } } } } } FindBugsReportWebJars .resources .filterKeys { it.endsWith(".js") } .filterKeys { !it.startsWith("prism/") } .forEach { path, content -> jsResource(content, path) } FindBugsReportWebJars .resources .filterKeys { it.endsWith(".js") } .filterKeys { path -> usedLanguages.any { path.startsWith("prism/$it.") } } .forEach { path, content -> jsResource(content, path) } } } private val File.sourceLanguage: String? get() = when (extension) { "as" -> "actionscript" "css" -> "css" "go" -> "go" "htm" -> "html" "html" -> "html" "groovy" -> "groovy" "ini" -> "ini" "java" -> "java" "js" -> "javascript" "json" -> "json" "kt" -> "kotlin" "less" -> "less" "lua" -> "lua" "md" -> "markdown" "php" -> "php" "properties" -> "properties" "proto" -> "protobuf" "protobuf" -> "protobuf" "py" -> "python" "sh" -> "bash" "jsx" -> "jsx" "tsx" -> "tsx" "rb" -> "ruby" "ruby" -> "ruby" "sass" -> "sass" "scss" -> "scss" "scala" -> "scala" "smarty" -> "smarty" "sql" -> "sql" "svg" -> "svg" "toml" -> "toml" "twig" -> "twig" "ts" -> "typescript" "xml" -> "xml" "yaml" -> "yaml" "yml" -> "yaml" else -> null } private fun buildHtmlString(builder: HTML.() -> Unit) = buildString { append("") appendHTML().html { lang = "en" builder(this) } } private fun FlowOrMetaDataContent.cssResource(content: ByteArray, path: String? = null) { style { path.nullIfEmpty()?.let { attributes["data-path"] = it } unsafe { raw(String(content, UTF_8)) } } } private fun FlowOrMetaDataContent.jsResource(content: ByteArray, path: String? = null) { script { path.nullIfEmpty()?.let { attributes["data-path"] = it } unsafe { raw(String(content, UTF_8)) } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy