SpotBugsPluginsTrait.groovy
/*
* Copyright 2005-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.codehaus.mojo.spotbugs
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.xml.XmlSlurper
import java.util.jar.JarEntry
import java.util.jar.JarFile
import org.apache.maven.RepositoryUtils
import org.apache.maven.artifact.Artifact
import org.apache.maven.execution.MavenSession
import org.apache.maven.plugin.logging.Log
import org.apache.maven.plugin.MojoExecutionException
import org.codehaus.plexus.resource.ResourceManager
import org.eclipse.aether.resolution.ArtifactRequest
import org.eclipse.aether.resolution.ArtifactResult
import org.xml.sax.SAXException
/**
* SpotBugs plugin support for Mojos.
*/
@CompileStatic
trait SpotBugsPluginsTrait {
// the trait needs certain objects to work, this need is expressed as abstract getters
// classes implement them with implicitly generated property getters
abstract org.eclipse.aether.RepositorySystem getRepositorySystem()
abstract org.apache.maven.repository.RepositorySystem getFactory()
abstract File getSpotbugsXmlOutputDirectory()
abstract Log getLog()
abstract ResourceManager getResourceManager()
// TODO This has been fixed for years now, apply as noted...
// properties in traits should be supported but don't compile currently:
// https://issues.apache.org/jira/browse/GROOVY-7536
// when fixed, should move pluginList and plugins properties here
abstract String getPluginList()
abstract List<PluginArtifact> getPlugins()
abstract List<Artifact> getPluginArtifacts()
abstract String getEffort()
abstract MavenSession getSession()
/**
* Adds the specified plugins to spotbugs. The coreplugin is always added first.
*
*/
String getSpotbugsPlugins() {
ResourceHelper resourceHelper = new ResourceHelper(log, new File(spotbugsXmlOutputDirectory, "spotbugs"), resourceManager)
List<String> urlPlugins = []
if (pluginList) {
log.debug(' Adding Plugins ')
pluginList.split(SpotBugsInfo.COMMA).each { String pluginJar ->
String pluginFileName = pluginJar.trim()
if (!pluginFileName.endsWith('.jar')) {
throw new MojoExecutionException("Plugin File is not a Jar file: ${pluginFileName}")
}
try {
if (log.isDebugEnabled()) {
log.debug(" Processing Plugin: ${pluginFileName}")
}
urlPlugins << resourceHelper.getResourceFile(pluginFileName).absolutePath
} catch (MalformedURLException e) {
throw new MojoExecutionException('The addin plugin has an invalid URL', e)
}
}
}
if (plugins) {
if (log.isDebugEnabled()) {
log.debug(' Adding Plugins from a repository')
log.debug(" Session is: ${session}")
}
plugins.each { PluginArtifact plugin ->
if (log.isDebugEnabled()) {
log.debug(" Processing Plugin: ${plugin}")
}
Artifact pomArtifact = plugin.classifier == null ?
this.factory.createArtifact(plugin.groupId, plugin.artifactId, plugin.version, "", plugin.type) :
this.factory.createArtifactWithClassifier(plugin.groupId, plugin.artifactId, plugin.version, plugin.type, plugin.classifier)
if (log.isDebugEnabled()) {
log.debug(" Added Artifact: ${pomArtifact}")
}
ArtifactRequest request = new ArtifactRequest(RepositoryUtils.toArtifact(pomArtifact), session.getCurrentProject().getRemoteProjectRepositories(), null)
ArtifactResult result = this.repositorySystem.resolveArtifact(session.getRepositorySession(), request)
pomArtifact.setFile(result.getArtifact().getFile())
urlPlugins << resourceHelper.getResourceFile(pomArtifact.file.absolutePath).absolutePath
}
}
// Auto-detect SpotBugs extension plugins added as standard Maven <dependencies> to the plugin.
// Any artifact on the plugin classpath (pluginArtifacts) that contains findbugs.xml
// and is not part of the SpotBugs core (com.github.spotbugs group) is treated as a plugin extension.
if (pluginArtifacts) {
if (log.isDebugEnabled()) {
log.debug(' Scanning plugin artifacts for SpotBugs extension plugins (added via <dependencies>)')
}
// Collect file names already in the plugin list to avoid adding the same JAR twice
// (e.g. when a plugin is declared both via <plugins> config and as a <dependency>).
Set<String> addedFileNames = urlPlugins.collect { new File(it).name } as Set
pluginArtifacts.each { Artifact artifact ->
if ('com.github.spotbugs' != artifact.groupId && artifact.file != null && isSpotBugsPlugin(artifact.file)) {
String jarFileName = artifact.file.name
if (!addedFileNames.contains(jarFileName)) {
if (log.isDebugEnabled()) {
log.debug(" Auto-detected SpotBugs extension plugin from dependency: ${artifact}")
}
addedFileNames << jarFileName
urlPlugins << resourceHelper.getResourceFile(artifact.file.absolutePath).absolutePath
}
}
}
}
String pluginListStr = urlPlugins.join(File.pathSeparator)
if (log.isDebugEnabled()) {
log.debug(" Plugin list is: ${pluginListStr}")
}
return pluginListStr
}
/**
* Determines whether the given file is a SpotBugs extension plugin by checking
* if it is a JAR containing {@code findbugs.xml} at the root.
*
* @param file the artifact file to inspect
* @return {@code true} if the file is a SpotBugs plugin JAR, {@code false} otherwise
*/
boolean isSpotBugsPlugin(File file) {
if (file == null || !file.exists() || !file.name.endsWith('.jar')) {
return false
}
try {
new JarFile(file).withCloseable { jar ->
return jar.getEntry('findbugs.xml') != null
}
} catch (IOException ignored) {
return false
}
}
/**
* Builds a mapping from bug type codes to their documentation URLs by reading
* the {@code findbugs.xml} descriptor from each resolved SpotBugs plugin JAR.
* <p>
* The method inspects JARs from two sources:
* <ol>
* <li>Plugin JARs listed in {@code pluginList} (comma-separated file paths).</li>
* <li>Plugin JARs discovered automatically from {@code pluginArtifacts} (Maven dependencies).</li>
* </ol>
* Built-in documentation URL templates are provided for the following well-known plugins:
* <ul>
* <li>{@code com.mebigfatguy.fbcontrib} → fb-contrib / sb-contrib</li>
* <li>{@code com.h3xstream.findsecbugs} → Find Security Bugs</li>
* </ul>
* User-supplied entries in {@code userPluginDocUrls} override the built-in defaults.
* URL templates may contain the placeholder {@code {type}} which will be replaced with
* the bug type code (e.g. {@code https://example.com/bugs.html#{type}}).
*
* @param userPluginDocUrls optional user-configured map of plugin IDs to URL templates
* @return a map from bug type code to fully-resolved documentation URL
*/
@CompileDynamic
Map<String, String> buildBugTypeUrlMap(Map<String, String> userPluginDocUrls) {
Map<String, String> defaults = [
'com.mebigfatguy.fbcontrib' : 'https://fb-contrib.sourceforge.net/bugdescriptions.html#{type}',
'com.h3xstream.findsecbugs' : 'https://find-sec-bugs.github.io/bugs.htm#{type}',
]
Map<String, String> effectiveUrls = defaults + (userPluginDocUrls ?: [:])
Map<String, String> bugTypeUrlMap = [:]
// Collect all candidate JAR files from pluginList and pluginArtifacts.
// We read the JARs directly (without copying) since we only need their metadata.
Set<File> pluginJars = []
if (pluginList) {
pluginList.split(SpotBugsInfo.COMMA).each { String path ->
String trimmed = path?.trim()
if (trimmed) {
File jar = new File(trimmed)
if (jar.exists()) pluginJars << jar
}
}
}
if (pluginArtifacts) {
pluginArtifacts.each { Artifact artifact ->
if ('com.github.spotbugs' != artifact.groupId && artifact.file != null && artifact.file.exists()) {
pluginJars << artifact.file
}
}
}
pluginJars.each { File pluginJar ->
try {
new JarFile(pluginJar).withCloseable { JarFile jar ->
JarEntry entry = jar.getEntry('findbugs.xml')
if (!entry) return
def xml = new XmlSlurper().parse(jar.getInputStream(entry))
String pluginId = xml.@pluginid.text()
String urlTemplate = effectiveUrls[pluginId]
if (!urlTemplate) return
xml.BugPattern.each { bugPattern ->
String type = bugPattern.@type.text()
if (type) {
bugTypeUrlMap[type] = urlTemplate.replace('{type}', type)
}
}
}
} catch (IOException | SAXException e) {
log.warn("Failed to read SpotBugs plugin JAR for URL mapping: ${pluginJar}: ${e.message}")
}
}
return bugTypeUrlMap
}
/**
* Returns the effort parameter to use.
*
* @return A valid effort parameter.
*
*/
String getEffortParameter() {
String effortParameter = (effort == 'Max') ? 'max' : (effort == 'Min') ? 'min' : 'default'
if (log.isDebugEnabled()) {
log.debug("effort is ${effort}")
log.debug("effortParameter is ${effortParameter}")
}
return "-effort:${effortParameter}"
}
}