org.sonarqube.gradle.SonarQubePlugin Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sonarqube-gradle-plugin Show documentation
Show all versions of sonarqube-gradle-plugin Show documentation
Gradle plugin to help analyzing projects with SonarQube
/**
* SonarQube Gradle Plugin
* Copyright (C) 2015-2017 SonarSource
* [email protected]
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package org.sonarqube.gradle;
import com.android.build.gradle.api.BaseVariant;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.gradle.api.Nullable;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.UnknownTaskException;
import org.gradle.api.internal.ConventionMapping;
import org.gradle.api.internal.plugins.DslObject;
import org.gradle.api.plugins.GroovyBasePlugin;
import org.gradle.api.plugins.GroovyPlugin;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.compile.JavaCompile;
import org.gradle.api.tasks.testing.Test;
import org.gradle.testing.jacoco.plugins.JacocoPlugin;
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension;
import org.sonarsource.scanner.api.Utils;
import static java.util.Arrays.asList;
/**
* A plugin for analyzing projects with the SonarQube Runner.
* When applied to a project, both the project itself and its subprojects will be analyzed (in a single run).
* Please see the “SonarQube Runner Plugin” chapter of the Gradle User Guide for more information.
*/
public class SonarQubePlugin implements Plugin {
private static final Pattern TEST_RESULT_FILE_PATTERN = Pattern.compile("TESTS?-.*\\.xml");
static final Predicate FILE_EXISTS = File::exists;
private static final Predicate IS_FILE = File::isFile;
static final String SONAR_SOURCES_PROP = "sonar.sources";
static final String SONAR_TESTS_PROP = "sonar.tests";
static final String SONAR_JAVA_SOURCE_PROP = "sonar.java.source";
static final String SONAR_JAVA_TARGET_PROP = "sonar.java.target";
private Project targetProject;
private static void evaluateSonarPropertiesBlocks(ActionBroadcast super SonarQubeProperties> propertiesActions, Map properties) {
SonarQubeProperties sqProperties = new SonarQubeProperties(properties);
propertiesActions.execute(sqProperties);
}
@Override
public void apply(Project project) {
targetProject = project;
final Map> actionBroadcastMap = new HashMap<>();
createTask(project, actionBroadcastMap);
ActionBroadcast actionBroadcast = addBroadcaster(actionBroadcastMap, project);
project.subprojects(p -> {
ActionBroadcast action = addBroadcaster(actionBroadcastMap, p);
p.getExtensions().create(SonarQubeExtension.SONARQUBE_EXTENSION_NAME, SonarQubeExtension.class, action);
});
project.getExtensions().create(SonarQubeExtension.SONARQUBE_EXTENSION_NAME, SonarQubeExtension.class, actionBroadcast);
}
private static ActionBroadcast addBroadcaster(Map> actionBroadcastMap, Project project) {
ActionBroadcast actionBroadcast = new ActionBroadcast<>();
actionBroadcastMap.put(project, actionBroadcast);
return actionBroadcast;
}
private SonarQubeTask createTask(final Project project, final Map> actionBroadcastMap) {
SonarQubeTask sonarQubeTask = project.getTasks().create(SonarQubeExtension.SONARQUBE_TASK_NAME, SonarQubeTask.class);
sonarQubeTask.setDescription("Analyzes " + project + " and its subprojects with SonarQube.");
ConventionMapping conventionMapping = new DslObject(sonarQubeTask).getConventionMapping();
conventionMapping.map("properties", () -> {
Map properties = new LinkedHashMap<>();
computeSonarProperties(project, properties, actionBroadcastMap, "");
return properties;
});
Callable> testTask = () -> project.getAllprojects().stream()
.filter(p -> p.getPlugins().hasPlugin(JavaPlugin.class) && !p.getExtensions().getByType(SonarQubeExtension.class).isSkipProject())
.map(p -> p.getTasks().getByName(JavaPlugin.TEST_TASK_NAME))
.collect(Collectors.toList());
sonarQubeTask.dependsOn(testTask);
Callable> callable = () -> project.getAllprojects().stream()
.filter(p -> isAndroidProject(p) && !p.getExtensions().getByType(SonarQubeExtension.class).isSkipProject())
.map(p -> {
BaseVariant variant = AndroidUtils.findVariant(p, p.getExtensions().getByType(SonarQubeExtension.class).getAndroidVariant());
List allCompileTasks = new ArrayList<>();
boolean unitTestTaskDepAdded = addTaskByName(p, "compile" + capitalize(variant.getName()) + "UnitTestJavaWithJavac", allCompileTasks);
boolean androidTestTaskDepAdded = addTaskByName(p, "compile" + capitalize(variant.getName()) + "AndroidTestJavaWithJavac", allCompileTasks);
// unit test compile and android test compile tasks already depends on main code compile so don't add a useless dependency
// that would lead to run main compile task several times
if (!unitTestTaskDepAdded && !androidTestTaskDepAdded) {
addTaskByName(p, "compile" + capitalize(variant.getName()) + "JavaWithJavac", allCompileTasks);
}
return allCompileTasks;
})
.flatMap(List::stream)
.collect(Collectors.toList());
sonarQubeTask.dependsOn(callable);
return sonarQubeTask;
}
private static boolean addTaskByName(Project p, String name, List allCompileTasks) {
try {
allCompileTasks.add(p.getTasks().getByName(name));
return true;
} catch (UnknownTaskException e) {
return false;
}
}
private static String capitalize(final String word) {
return Character.toUpperCase(word.charAt(0)) + word.substring(1);
}
private void computeSonarProperties(Project project, Map properties, Map> sonarPropertiesActionBroadcastMap,
String prefix) {
SonarQubeExtension extension = project.getExtensions().getByType(SonarQubeExtension.class);
if (extension.isSkipProject()) {
return;
}
Map rawProperties = new LinkedHashMap<>();
addGradleDefaults(project, rawProperties);
if (isAndroidProject(project)) {
AndroidUtils.configureForAndroid(project, extension.getAndroidVariant(), rawProperties);
}
evaluateSonarPropertiesBlocks(sonarPropertiesActionBroadcastMap.get(project), rawProperties);
if (project.equals(targetProject)) {
addEnvironmentProperties(rawProperties);
addSystemProperties(rawProperties);
}
rawProperties.putIfAbsent(SONAR_SOURCES_PROP, "");
convertProperties(rawProperties, prefix, properties);
List enabledChildProjects = project.getChildProjects().values().stream()
.filter(p -> !p.getExtensions().getByType(SonarQubeExtension.class).isSkipProject())
.collect(Collectors.toList());
if (enabledChildProjects.isEmpty()) {
return;
}
List moduleIds = new ArrayList<>();
for (Project childProject : enabledChildProjects) {
String moduleId = childProject.getPath();
moduleIds.add(moduleId);
String modulePrefix = (prefix.length() > 0) ? (prefix + "." + moduleId) : moduleId;
computeSonarProperties(childProject, properties, sonarPropertiesActionBroadcastMap, modulePrefix);
}
properties.put(convertKey("sonar.modules", prefix), moduleIds.stream().collect(Collectors.joining(",")));
}
private void addGradleDefaults(final Project project, final Map properties) {
properties.put("sonar.projectName", project.getName());
properties.put("sonar.projectDescription", project.getDescription());
properties.put("sonar.projectVersion", project.getVersion());
properties.put("sonar.projectBaseDir", project.getProjectDir());
if (project.equals(targetProject)) {
// Root project
properties.put("sonar.projectKey", getProjectKey(project));
properties.put("sonar.working.directory", new File(project.getBuildDir(), "sonar"));
} else {
properties.put("sonar.moduleKey", getProjectKey(project));
}
configureForJava(project, properties);
configureForGroovy(project, properties);
}
private static boolean isAndroidProject(Project project) {
return project.getPlugins().hasPlugin("com.android.application") || project.getPlugins().hasPlugin("com.android.library") || project.getPlugins().hasPlugin("com.android.test");
}
private static void configureForJava(final Project project, final Map properties) {
project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> configureJdkSourceAndTarget(project, properties));
project.getPlugins().withType(JavaPlugin.class, javaPlugin -> {
boolean hasSourceOrTest = configureSourceDirsAndJavaClasspath(project, properties, false);
if (hasSourceOrTest) {
configureSourceEncoding(project, properties);
final Test testTask = (Test) project.getTasks().getByName(JavaPlugin.TEST_TASK_NAME);
configureTestReports(testTask, properties);
configureJaCoCoCoverageReport(testTask, false, project, properties);
}
});
}
/**
* Groovy projects support joint compilation of a mix of Java and Groovy classes. That's why we set both
* sonar.java.* and sonar.groovy.* properties.
*/
private static void configureForGroovy(final Project project, final Map properties) {
project.getPlugins().withType(GroovyBasePlugin.class, groovyBasePlugin -> configureJdkSourceAndTarget(project, properties));
project.getPlugins().withType(GroovyPlugin.class, groovyPlugin -> {
boolean hasSourceOrTest = configureSourceDirsAndJavaClasspath(project, properties, true);
if (hasSourceOrTest) {
configureSourceEncoding(project, properties);
final Test testTask = (Test) project.getTasks().getByName(JavaPlugin.TEST_TASK_NAME);
configureTestReports(testTask, properties);
configureJaCoCoCoverageReport(testTask, true, project, properties);
}
});
}
private static void configureJaCoCoCoverageReport(final Test testTask, final boolean addForGroovy, Project project, final Map properties) {
project.getPlugins().withType(JacocoPlugin.class, jacocoPlugin -> {
JacocoTaskExtension jacocoTaskExtension = testTask.getExtensions().getByType(JacocoTaskExtension.class);
File destinationFile = jacocoTaskExtension.getDestinationFile();
if (destinationFile.exists()) {
properties.put("sonar.jacoco.reportPath", destinationFile);
if (addForGroovy) {
properties.put("sonar.groovy.jacoco.reportPath", destinationFile);
}
}
});
}
private static void configureTestReports(Test testTask, Map properties) {
File testResultsDir = testTask.getReports().getJunitXml().getDestination();
// do not set a custom test reports path if it does not exists, otherwise SonarQube will emit an error
// do not set a custom test reports path if there are no files, otherwise SonarQube will emit a warning
if (testResultsDir.isDirectory()
&& asList(testResultsDir.list()).stream().anyMatch(file -> TEST_RESULT_FILE_PATTERN.matcher(file).matches())) {
properties.put("sonar.junit.reportsPath", testResultsDir);
// For backward compatibility
properties.put("sonar.surefire.reportsPath", testResultsDir);
}
}
private static boolean configureSourceDirsAndJavaClasspath(Project project, Map properties, final boolean addForGroovy) {
JavaPluginConvention javaPluginConvention = new DslObject(project).getConvention().getPlugin(JavaPluginConvention.class);
SourceSet main = javaPluginConvention.getSourceSets().getAt("main");
List sourceDirectories = nonEmptyOrNull(main.getAllSource().getSrcDirs().stream().filter(FILE_EXISTS).collect(Collectors.toList()));
properties.put(SONAR_SOURCES_PROP, sourceDirectories);
SourceSet test = javaPluginConvention.getSourceSets().getAt("test");
List testDirectories = nonEmptyOrNull(test.getAllSource().getSrcDirs().stream().filter(FILE_EXISTS).collect(Collectors.toList()));
properties.put(SONAR_TESTS_PROP, testDirectories);
File mainClassDir = main.getOutput().getClassesDir();
Collection mainLibraries = getLibraries(main);
setMainClasspathProps(properties, addForGroovy, mainClassDir, mainLibraries);
File testClassDir = test.getOutput().getClassesDir();
Collection testLibraries = getLibraries(test);
setTestClasspathProps(properties, testClassDir, testLibraries);
return sourceDirectories != null || testDirectories != null;
}
static void setMainClasspathProps(Map properties, boolean addForGroovy, @Nullable File mainClassDir, Collection mainLibraries) {
if (mainClassDir != null && mainClassDir.exists()) {
appendProp(properties, "sonar.java.binaries", mainClassDir);
if (addForGroovy) {
appendProp(properties, "sonar.groovy.binaries", mainClassDir);
}
// Populate deprecated properties for backward compatibility
appendProp(properties, "sonar.binaries", mainClassDir);
}
appendProps(properties, "sonar.java.libraries", mainLibraries);
// Populate deprecated properties for backward compatibility
appendProps(properties, "sonar.libraries", mainLibraries);
}
static void appendProps(Map properties, String key, Iterable valuesToAppend) {
properties.putIfAbsent(key, new LinkedHashSet());
StreamSupport.stream(valuesToAppend.spliterator(), false)
.forEach(v -> ((Collection) properties.get(key)).add(v.toString()));
}
static void appendProp(Map properties, String key, Object valueToAppend) {
properties.putIfAbsent(key, new LinkedHashSet());
((Collection) properties.get(key)).add(valueToAppend.toString());
}
static void setTestClasspathProps(Map properties, @Nullable File testClassDir, Collection testLibraries) {
if (testClassDir != null && testClassDir.exists()) {
appendProp(properties, "sonar.java.test.binaries", testClassDir);
}
appendProps(properties, "sonar.java.test.libraries", testLibraries);
}
private static void configureSourceEncoding(Project project, final Map properties) {
project.getTasks().withType(JavaCompile.class, compile -> {
String encoding = compile.getOptions().getEncoding();
if (encoding != null) {
properties.put("sonar.sourceEncoding", encoding);
}
});
}
private static void configureJdkSourceAndTarget(Project project, Map properties) {
JavaPluginConvention javaPluginConvention = new DslObject(project).getConvention().getPlugin(JavaPluginConvention.class);
properties.put(SONAR_JAVA_SOURCE_PROP, javaPluginConvention.getSourceCompatibility());
properties.put(SONAR_JAVA_TARGET_PROP, javaPluginConvention.getTargetCompatibility());
}
private static String getProjectKey(Project project) {
Project rootProject = project.getRootProject();
String rootProjectName = rootProject.getName();
String rootGroup = rootProject.getGroup().toString();
String rootKey = rootGroup.isEmpty() ? rootProjectName : (rootGroup + ":" + rootProjectName);
if (project == rootProject) {
return rootKey;
}
return rootKey + project.getPath();
}
private static void addEnvironmentProperties(Map properties) {
for (Map.Entry