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

name.remal.gradle_plugins.plugins.code_quality.findbugs.BaseFindBugsSettingsPlugin.kt Maven / Gradle / Ivy

There is a newer version: 1.9.2
Show newest version
package name.remal.gradle_plugins.plugins.code_quality.findbugs

import com.google.common.reflect.TypeToken
import name.remal.*
import name.remal.gradle_plugins.dsl.BaseReflectiveProjectPlugin
import name.remal.gradle_plugins.dsl.CreateExtensionsPluginAction
import name.remal.gradle_plugins.dsl.PluginAction
import name.remal.gradle_plugins.dsl.WithPluginClasses
import name.remal.gradle_plugins.dsl.extensions.*
import name.remal.gradle_plugins.dsl.utils.ProjectAware
import name.remal.gradle_plugins.dsl.utils.XML_PRETTY_OUTPUTTER
import name.remal.gradle_plugins.plugins.classes_relocation.ClassesRelocationPlugin
import name.remal.gradle_plugins.plugins.classes_relocation.relocatedClassesJavaPackageName
import name.remal.gradle_plugins.plugins.code_quality.ExcludesExtension
import name.remal.gradle_plugins.plugins.code_quality.findbugs.extensions.FindBugsSubplugin
import name.remal.gradle_plugins.plugins.code_quality.setupQualityTaskReporters
import name.remal.gradle_plugins.plugins.dependencies.TransitiveDependenciesExtension
import name.remal.gradle_plugins.plugins.dependencies.TransitiveDependenciesPlugin
import name.remal.version.Version
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.ConfigurationContainer
import org.gradle.api.artifacts.ExternalModuleDependency
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.file.FileCollection
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.ExtensionContainer
import org.gradle.api.plugins.quality.CodeQualityExtension
import org.gradle.api.reporting.Reporting
import org.gradle.api.tasks.TaskContainer
import org.gradle.initialization.DefaultSettings.DEFAULT_BUILD_SRC_DIR
import org.jdom2.Document
import org.jdom2.Element
import org.jdom2.input.SAXBuilder
import java.io.File
import java.lang.reflect.ParameterizedType
import kotlin.collections.forEach

abstract class BaseFindBugsSettingsPlugin(
    private val objectFactory: ObjectFactory
) : BaseReflectiveProjectPlugin() {

    companion object {
        @JvmStatic
        protected val FINDBUGS_EXCLUDE_RESOURCE_URL = BaseFindBugsSettingsPlugin::class.java.getRequiredResource("exclude.xml")

        private fun newSAXBuilder() = SAXBuilder().apply {
            setNoValidatingXMLReaderFactory()
            setNoOpEntityResolver()
        }
    }


    protected lateinit var project: Project

    @PluginAction(order = Int.MIN_VALUE, isHidden = true)
    private fun populateProject(project: Project) {
        this.project = project
    }


    protected abstract val toolName: String

    protected open val extensionName: String get() = toolName.toLowerCase()

    protected open val toolConfigurationName: String get() = extensionName
    protected open val pluginsConfigurationName: String get() = "${extensionName}Plugins"

    protected abstract val toolLatestVersion: String

    protected abstract val toolArtifactGroup: String
    protected abstract val toolArtifactIds: Set

    protected abstract var TaskType.excludeFilterFile: File?

    protected abstract val TaskType.classesDirs: FileCollection


    protected val extensionType: Class = run {
        val type = TypeToken.of(this.javaClass).getSupertype(BaseFindBugsSettingsPlugin::class.java).type
        if (type !is ParameterizedType) throw IllegalStateException("$type is not instance of ParameterizedType")
        return@run type.actualTypeArguments[0].asClass().uncheckedCast>()
    }

    protected val taskType: Class = run {
        val type = TypeToken.of(this.javaClass).getSupertype(BaseFindBugsSettingsPlugin::class.java).type
        if (type !is ParameterizedType) throw IllegalStateException("$type is not instance of ParameterizedType")
        return@run type.actualTypeArguments[1].asClass().uncheckedCast>()
    }


    @CreateExtensionsPluginAction
    fun ExtensionContainer.`Create 'excludes' extension for tool extension`() {
        toolExtension.convention.add(
            "excludes",
            objectFactory.newInstance(ExcludesExtension::class.java)
        )
    }

    @CreateExtensionsPluginAction
    fun TaskContainer.`Create 'excludes' extension for each task`() {
        all(taskType) { task ->
            task.extensions.add(
                "excludes",
                objectFactory.newInstance(ExcludesExtension::class.java)
            )
        }
    }

    @PluginAction
    @WithPluginClasses(ClassesRelocationPlugin::class)
    fun ExtensionContainer.`Don't check relocated classes`(project: Project) {
        globalExcludes.className(project.relocatedClassesJavaPackageName + ".*")
    }

    @PluginAction
    fun ExtensionContainer.`Update tool version`() {
        val toolExtension = this.toolExtension
        val toolVersionStr = toolExtension.toolVersion
        if (toolVersionStr.isNullOrEmpty()) {
            toolExtension.toolVersion = toolLatestVersion

        } else {
            val toolVersion = Version.parseOrNull(toolVersionStr)
            val buildVersion = Version.parseOrNull(toolLatestVersion)
            if (toolVersion != null && buildVersion != null) {
                if (toolVersion < buildVersion) {
                    toolExtension.toolVersion = buildVersion.toString()
                }
            }
        }
    }

    @PluginAction
    fun ConfigurationContainer.`Force tool dependencies to have the same version as toolVersion`(extensions: ExtensionContainer, dependencyHandler: DependencyHandler) {
        val toolExtension = extensions.toolExtension
        all {
            it.resolutionStrategy {
                it.eachDependency { dep ->
                    val toolVersion = toolExtension.toolVersion.nullIfEmpty() ?: return@eachDependency
                    with(dep.target) {
                        if (group == toolArtifactGroup && name in toolArtifactIds) {
                            if (version.isNullOrEmpty()) {
                                dep.useVersion(toolVersion)
                            }
                        }
                    }
                }
            }
            it.beforeResolve { conf ->
                val toolVersion = toolExtension.toolVersion.nullIfEmpty() ?: return@beforeResolve
                val usedVersions = mutableMapOf>()
                val dependenciesToForce = mutableListOf()
                val forcedDependencies = mutableListOf()
                conf.dependencies.forEach { dep ->
                    if (dep !is ExternalModuleDependency) return@forEach
                    if (dep.group != toolArtifactGroup || dep.name !in toolArtifactIds) return@forEach
                    val depKey = "${dep.group}:${dep.name}"
                    usedVersions.computeIfAbsent(depKey, { mutableSetOf() }).add(dep.version.default())
                    if (dep.isForce) {
                        forcedDependencies.add(depKey)
                    } else if (!dep.versionConstraint.preferredVersion.isNullOrEmpty()) {
                        forcedDependencies.add(depKey)
                        usedVersions.computeIfAbsent(depKey, { mutableSetOf() }).add(dep.versionConstraint.preferredVersion)
                    } else {
                        dependenciesToForce.add(depKey)
                    }
                }
                dependenciesToForce.removeAll(forcedDependencies)
                dependenciesToForce.forEach { depKey ->
                    if (usedVersions[depKey]?.all { it == toolVersion } == true) return@forEach
                    conf.dependencies.add(dependencyHandler.create("$depKey:$toolVersion") {
                        it.isForce = true
                    })
                }
            }
        }
    }

    @PluginAction
    fun TaskContainer.`No-source if no classes exist`() {
        all(taskType) {
            it.noSourceIf { it.classesDirs.isEmpty }
        }
    }

    @PluginAction
    fun TaskContainer.`Setup task default filters`(project: Project) {
        all(taskType) {
            it.doSetup { task ->
                if (task.excludeFilterFile == null) {
                    var rootDir: File = project.rootDir
                    if (rootDir.name == DEFAULT_BUILD_SRC_DIR) {
                        rootDir = rootDir.parentFile
                    }
                    val configFile = File(rootDir, "gradle/$extensionName/exclude.xml")
                    if (!configFile.isFile) {
                        synchronized(BaseFindBugsSettingsPlugin::class.java) {
                            if (!configFile.isFile) {
                                configFile.forceDeleteRecursively().createParentDirectories()
                                FINDBUGS_EXCLUDE_RESOURCE_URL.openStream().use { inputStream ->
                                    configFile.outputStream().use { outputStream ->
                                        inputStream.copyTo(outputStream)
                                    }
                                }
                            }
                        }
                    }
                    task.excludeFilterFile = configFile
                }
            }
        }
    }

    @PluginAction
    @Suppress("ComplexMethod", "LongMethod")
    fun TaskContainer.`Apply exclude rules`(extensions: ExtensionContainer) {
        val globalExcludes = extensions.globalExcludes
        all(taskType) {
            it.doSetup(Int.MAX_VALUE) { task ->
                val taskExcludes = task.taskExcludes
                val classNames = sequenceOf(globalExcludes.classNames, taskExcludes.classNames).flatten().toSortedSet()
                val sourcePaths = sequenceOf(globalExcludes.sources, taskExcludes.sources).flatten().toSortedSet()
                val messages = sequenceOf(globalExcludes.messages, taskExcludes.messages).flatten().toSortedSet()
                if (classNames.isEmpty() && sourcePaths.isEmpty() && messages.isEmpty()) return@doSetup

                val newExcludeXmlFile = newTempFile("$extensionName-exclude-", ".xml")
                task.doFirst {
                    val document = task.excludeFilterFile?.let { newSAXBuilder().build(it).clearNamespaces() }
                        ?: Document().setRootElement(Element("FindBugsFilter"))

                    if (classNames.isNotEmpty()) {
                        document.rootElement.addContent(
                            Element("Match").addContent(
                                Element("Or").also { element ->
                                    classNames.forEach { className ->
                                        element.addContent(
                                            Element("Class").setAttribute(
                                                "name",
                                                if (className.contains('*')) {
                                                    "~" + className.splitToSequence('*')
                                                        .map(::escapeRegex)
                                                        .joinToString(".*")
                                                } else {
                                                    className
                                                }
                                            )
                                        )
                                    }
                                }
                            )
                        )
                    }

                    if (sourcePaths.isNotEmpty()) {
                        document.rootElement.addContent(
                            Element("Match").addContent(
                                Element("Or").also { element ->
                                    sourcePaths.forEach { sourcePath ->
                                        element.addContent(
                                            Element("Source").setAttribute(
                                                "name",
                                                if (sourcePath.contains('*')) {
                                                    "~" + sourcePath.splitToSequence('*')
                                                        .map(::escapeRegex)
                                                        .joinToString(".*")
                                                } else {
                                                    sourcePath
                                                }
                                            )
                                        )
                                    }
                                }
                            )
                        )
                    }

                    if (messages.isNotEmpty()) {
                        document.rootElement.addContent(
                            Element("Match").addContent(
                                Element("Or").also { element ->
                                    messages.forEach { message ->
                                        element.addContent(
                                            Element("Bug").setAttribute(
                                                "name",
                                                if (message.contains('*')) {
                                                    "~" + message.splitToSequence('*')
                                                        .map(::escapeRegex)
                                                        .joinToString(".*")
                                                } else {
                                                    message
                                                }
                                            )
                                        )
                                    }
                                }
                            )
                        )
                    }

                    newExcludeXmlFile.outputStream().use { outputStream ->
                        XML_PRETTY_OUTPUTTER.output(document, outputStream)
                    }

                    task.excludeFilterFile = newExcludeXmlFile
                }

                task.doLast {
                    newExcludeXmlFile.delete()
                }
            }
        }
    }

    @PluginAction
    fun TaskContainer.`Setup task reports`() {
        all(taskType) {
            setupQualityTaskReporters(it)
            it.doSetup { task ->
                task as Reporting<*>
                task.reports.forEach { report ->
                    if ("xml" == report.name) {
                        report.isEnabled = true

                        try {
                            report.javaClass.findMethod("setWithMessages", Boolean::class.java)
                                ?.apply { isAccessible = true }
                                ?.invoke(report, true)
                        } catch (e: Exception) {
                            logger.debug(e)
                        }

                    } else {
                        report.isEnabled = false
                    }
                }
            }
        }
    }

    @PluginAction
    fun ExtensionContainer.`Add plugin extensions to the plugin extension`(dependencyHandler: DependencyHandler, project: Project) {
        val toolExtension = this.toolExtension
        loadServices(FindBugsSubplugin::class.java).toSortedSet().forEach { findBugsSubplugin ->
            if (findBugsSubplugin is ProjectAware) {
                findBugsSubplugin.project = project
            }
            toolExtension.conventionWithSelf.addExtensionMethod(findBugsSubplugin.extensionName) {
                dependencyHandler.add(pluginsConfigurationName, findBugsSubplugin.dependencyNotation)
            }
        }
    }

    @PluginAction
    fun TaskContainer.`Create plugins help task`() {
        create("${extensionName}PluginsHelp", DisplayFindBugsPluginsHelp::class.java) {
            it.toolName = [email protected]
        }
    }

    @PluginAction
    fun ConfigurationContainer.`Exclude tool from plugins configuration`() {
        this[pluginsConfigurationName].apply {
            exclude("com.google.code.findbugs", "findbugs")
            exclude("com.google.code.findbugs", "annotations")
            exclude("com.google.code.findbugs", "jsr305")
            exclude("com.google.spotbugs", "spotbugs")
            exclude("com.google.spotbugs", "spotbugs-annotations")
            exclude("com.google.spotbugs", "annotations")
        }
    }

    @PluginAction
    @WithPluginClasses(TransitiveDependenciesPlugin::class)
    fun ExtensionContainer.`Register tool configurations in 'transitiveDependencies' extension`() {
        this[TransitiveDependenciesExtension::class.java].addConfigurationToProcess(
            toolConfigurationName,
            pluginsConfigurationName
        )
    }


    protected val ExtensionContainer.toolExtension: ExtensionType get() = this[extensionType]
    protected val ExtensionContainer.globalExcludes: ExcludesExtension get() = toolExtension.convention[ExcludesExtension::class.java]
    protected val TaskType.taskExcludes: ExcludesExtension get() = this[ExcludesExtension::class.java]

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy