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.asClass
import name.remal.clearNamespaces
import name.remal.createParentDirectories
import name.remal.debug
import name.remal.default
import name.remal.escapeRegex
import name.remal.findMethod
import name.remal.forceDeleteRecursively
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.addExtensionMethod
import name.remal.gradle_plugins.dsl.extensions.all
import name.remal.gradle_plugins.dsl.extensions.beforeResolve
import name.remal.gradle_plugins.dsl.extensions.convention
import name.remal.gradle_plugins.dsl.extensions.conventionWithSelf
import name.remal.gradle_plugins.dsl.extensions.create
import name.remal.gradle_plugins.dsl.extensions.doSetup
import name.remal.gradle_plugins.dsl.extensions.exclude
import name.remal.gradle_plugins.dsl.extensions.get
import name.remal.gradle_plugins.dsl.extensions.getRequiredResource
import name.remal.gradle_plugins.dsl.extensions.noSourceIf
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.loadServices
import name.remal.newTempFile
import name.remal.nullIfEmpty
import name.remal.setNoOpEntityResolver
import name.remal.setNoValidatingXMLReaderFactory
import name.remal.uncheckedCast
import name.remal.use
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