[gradle] import annotation processing output folders, properly mark them as "generated sources"

Fixes/implements IDEA-124090 IDEA-182577 IDEA-208205

GitOrigin-RevId: 00c483a947a908a47c6100d20bf8976d1103a423
This commit is contained in:
Nikita Skvortsov
2019-10-03 12:34:25 +00:00
committed by intellij-monorepo-bot
parent d7d3e7d38f
commit 05db0f3fd5
10 changed files with 282 additions and 51 deletions
@@ -14,6 +14,8 @@ import static com.intellij.util.containers.ContainerUtil.immutableList;
public class AnnotationProcessingData {
public static final Key<AnnotationProcessingData> KEY = Key.create(AnnotationProcessingData.class, ExternalSystemConstants.UNORDERED);
public static final Key<AnnotationProcessorOutput> OUTPUT_KEY =
Key.create(AnnotationProcessorOutput.class, ExternalSystemConstants.UNORDERED);
private static final WeakInterner<AnnotationProcessingData> ourInterner = new WeakInterner<>();
@@ -47,4 +49,24 @@ public class AnnotationProcessingData {
public Collection<String> getPath() {
return path;
}
public static class AnnotationProcessorOutput {
private final String outputPath;
private final boolean testSources;
@PropertyMapping({"outputPath", "testSources"})
public AnnotationProcessorOutput(@NotNull String path, boolean isTestSources) {
outputPath = path;
testSources = isTestSources;
}
@NotNull
public String getOutputPath() {
return outputPath;
}
public boolean isTestSources() {
return testSources;
}
}
}
@@ -89,18 +89,40 @@ public class JavaGradleProjectResolver extends AbstractProjectResolverExtension
}
if (!resolverCtx.isResolveModulePerSourceSet()) {
final AnnotationProcessingData apData = getMergedAnnotationProcessingData(apModel);
ideModule.createChild(AnnotationProcessingData.KEY, apData);
DataNode<AnnotationProcessingData> dataNode = ideModule.createChild(AnnotationProcessingData.KEY, apData);
populateAnnotationProcessingOutput(dataNode, apModel);
} else {
Collection<DataNode<GradleSourceSetData>> all = ExternalSystemApiUtil.findAll(ideModule, GradleSourceSetData.KEY);
for (DataNode<GradleSourceSetData> node : all) {
final AnnotationProcessingData apData = getAnnotationProcessingData(apModel, node.getData().getModuleName());
if (apData != null) {
node.createChild(AnnotationProcessingData.KEY, apData);
DataNode<AnnotationProcessingData> dataNode = node.createChild(AnnotationProcessingData.KEY, apData);
populateAnnotationProcessorOutput(dataNode, apModel, node.getData().getModuleName());
}
}
}
}
private static void populateAnnotationProcessorOutput(@NotNull DataNode<AnnotationProcessingData> parent,
@NotNull AnnotationProcessingModel apModel,
@NotNull String sourceSetName) {
AnnotationProcessingConfig config = apModel.bySourceSetName(sourceSetName);
if (config != null && config.getProcessorOutput() != null) {
parent.createChild(AnnotationProcessingData.OUTPUT_KEY,
new AnnotationProcessingData.AnnotationProcessorOutput(config.getProcessorOutput(), config.isTestSources()));
}
}
private static void populateAnnotationProcessingOutput(@NotNull DataNode<AnnotationProcessingData> parent,
@NotNull AnnotationProcessingModel apModel) {
for (AnnotationProcessingConfig config : apModel.allConfigs().values()) {
if (config.getProcessorOutput() != null) {
parent.createChild(AnnotationProcessingData.OUTPUT_KEY,
new AnnotationProcessingData.AnnotationProcessorOutput(config.getProcessorOutput(), config.isTestSources()));
}
}
}
@NotNull
private static AnnotationProcessingData getMergedAnnotationProcessingData(@NotNull AnnotationProcessingModel apModel) {
@@ -3,6 +3,7 @@ package org.jetbrains.plugins.gradle.service.project.data
import com.intellij.compiler.CompilerConfiguration
import com.intellij.compiler.CompilerConfigurationImpl
import com.intellij.ide.projectView.actions.MarkRootActionBase
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.externalSystem.model.DataNode
import com.intellij.openapi.externalSystem.model.Key
@@ -10,18 +11,36 @@ import com.intellij.openapi.externalSystem.model.project.ModuleData
import com.intellij.openapi.externalSystem.model.project.ProjectData
import com.intellij.openapi.externalSystem.service.project.IdeModifiableModelsProvider
import com.intellij.openapi.externalSystem.service.project.manage.AbstractProjectDataService
import com.intellij.openapi.externalSystem.service.project.manage.SourceFolderManager
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.externalSystem.util.ExternalSystemConstants
import com.intellij.openapi.externalSystem.util.Order
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ContentEntry
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.roots.SourceFolder
import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFileManager
import org.jetbrains.jps.model.java.JavaSourceRootType
import org.jetbrains.jps.model.java.JpsJavaExtensionService
import org.jetbrains.jps.model.java.compiler.ProcessorConfigProfile
import org.jetbrains.jps.model.java.impl.compiler.ProcessorConfigProfileImpl
import org.jetbrains.plugins.gradle.model.data.AnnotationProcessingData
import org.jetbrains.plugins.gradle.settings.GradleSettings
import java.io.File
import java.util.*
import kotlin.collections.ArrayList
@Order(ExternalSystemConstants.UNORDERED)
class AnnotationProcessingDataService : AbstractProjectDataService<AnnotationProcessingData, ProcessorConfigProfile>() {
private lateinit var sourceFolderManager: SourceFolderManager
private lateinit var modelsProvider: IdeModifiableModelsProvider
override fun getTargetDataKey(): Key<AnnotationProcessingData> {
return AnnotationProcessingData.KEY
}
@@ -29,9 +48,11 @@ class AnnotationProcessingDataService : AbstractProjectDataService<AnnotationPro
override fun importData(toImport: Collection<DataNode<AnnotationProcessingData>>,
projectData: ProjectData?,
project: Project,
modelsProvider: IdeModifiableModelsProvider) {
modifiableModelsProvider: IdeModifiableModelsProvider) {
val importedData = mutableSetOf<AnnotationProcessingData>()
val config = CompilerConfiguration.getInstance(project) as CompilerConfigurationImpl
sourceFolderManager = SourceFolderManager.getInstance(project)
modelsProvider = modifiableModelsProvider
for (node in toImport) {
val moduleData = node.parent?.data as? ModuleData
if (moduleData == null) {
@@ -46,9 +67,126 @@ class AnnotationProcessingDataService : AbstractProjectDataService<AnnotationPro
}
config.configureAnnotationProcessing(ideModule, node.data, importedData)
if (projectData != null) {
val isDelegatedBuild = GradleSettings.getInstance(project).getLinkedProjectSettings(
projectData.linkedExternalProjectPath)?.delegatedBuild ?: true
clearGeneratedSourceFolders(ideModule, node)
addGeneratedSourceFolders(ideModule, node, isDelegatedBuild)
}
}
}
private fun clearGeneratedSourceFolders(ideModule: Module,
node: DataNode<AnnotationProcessingData>) {
val gradleOutputs = ExternalSystemApiUtil.findAll(node, AnnotationProcessingData.OUTPUT_KEY)
val pathsToRemove =
(gradleOutputs
.map { it.data.outputPath } +
listOf(
getAnnotationProcessorGenerationPath(ideModule, false),
getAnnotationProcessorGenerationPath(ideModule, true)
))
.filterNotNull()
pathsToRemove.forEach { path ->
val url = VfsUtilCore.pathToUrl(path)
val modifiableRootModel = modelsProvider.getModifiableRootModel(ideModule)
val (entry, folder) = findContentEntryOrFolder(modifiableRootModel, url)
if (entry != null) {
if (folder != null) {
entry.removeSourceFolder(folder)
}
if (entry.sourceFolders.isEmpty()) {
modifiableRootModel.removeContentEntry(entry)
}
}
}
}
private fun addGeneratedSourceFolders(ideModule: Module,
node: DataNode<AnnotationProcessingData>,
delegatedBuild: Boolean) {
if (delegatedBuild) {
val outputs = ExternalSystemApiUtil.findAll(node, AnnotationProcessingData.OUTPUT_KEY)
outputs.forEach {
val outputPath = it.data.outputPath
val isTestSource = it.data.isTestSources
addGeneratedSourceFolder(ideModule, outputPath, isTestSource)
}
}
else {
val outputPath = getAnnotationProcessorGenerationPath(ideModule, false)
if (outputPath != null) {
addGeneratedSourceFolder(ideModule, outputPath, false)
}
val testOutputPath = getAnnotationProcessorGenerationPath(ideModule, true)
if (testOutputPath != null) {
addGeneratedSourceFolder(ideModule, testOutputPath, true)
}
}
}
private fun addGeneratedSourceFolder(ideModule: Module,
path: String,
isTest: Boolean) {
val type = if (isTest) JavaSourceRootType.TEST_SOURCE else JavaSourceRootType.SOURCE
val url = VfsUtilCore.pathToUrl(path)
val vf = LocalFileSystem.getInstance().refreshAndFindFileByPath(path)
if (vf == null || !vf.exists()) {
sourceFolderManager.addSourceFolder(ideModule, url, type)
sourceFolderManager.setSourceFolderGenerated(url, true)
} else {
val modifiableRootModel = modelsProvider.getModifiableRootModel(ideModule)
val contentEntry = MarkRootActionBase.findContentEntry(modifiableRootModel, vf)
?: modifiableRootModel.addContentEntry(url)
val properties = JpsJavaExtensionService.getInstance().createSourceRootProperties("", true)
contentEntry.addSourceFolder(url, type, properties)
}
}
private fun findContentEntryOrFolder(modifiableRootModel: ModifiableRootModel,
url: String): Pair<ContentEntry?, SourceFolder?> {
var entryVar: ContentEntry? = null
var folderVar: SourceFolder? = null
modifiableRootModel.contentEntries.forEach search@{ ce ->
ce.sourceFolders.forEach { sf ->
if (sf.url == url) {
entryVar = ce
folderVar = sf
return@search
}
}
if (ce.url == url) {
entryVar = ce
}
}
return entryVar to folderVar
}
private fun getAnnotationProcessorGenerationPath(ideModule: Module, forTests: Boolean): String? {
val config = CompilerConfiguration.getInstance(ideModule.project).getAnnotationProcessingConfiguration(ideModule)
val sourceDirName = config.getGeneratedSourcesDirectoryName(forTests)
val roots = modelsProvider.getModifiableRootModel(ideModule).contentRootUrls
if (roots.isEmpty()) {
return null
}
if (roots.size > 1) {
Arrays.sort(roots)
}
return if (StringUtil.isEmpty(sourceDirName)) VirtualFileManager.extractPath(roots[0])
else VirtualFileManager.extractPath(roots[0]) + "/" + sourceDirName
}
override fun computeOrphanData(toImport: MutableCollection<DataNode<AnnotationProcessingData>>,
projectData: ProjectData,
project: Project,
@@ -94,6 +232,7 @@ class AnnotationProcessingDataService : AbstractProjectDataService<AnnotationPro
with(profile) {
isEnabled = true
isObtainProcessorsFromClasspath = false
isOutputRelativeToContentRoot = true
addModuleName(ideModule.name)
}
}
@@ -4,6 +4,7 @@ package org.jetbrains.plugins.gradle.importing
import com.intellij.compiler.CompilerConfiguration
import com.intellij.compiler.CompilerConfigurationImpl
import org.assertj.core.api.BDDAssertions.then
import org.jetbrains.plugins.gradle.settings.GradleSettings
import org.jetbrains.plugins.gradle.tooling.annotation.TargetVersions
import org.junit.Test
@@ -134,6 +135,54 @@ class AnnotationProcessorConfigImportingTest: GradleImportingTestCase() {
}
}
@Test
@TargetVersions("5.2+")
fun `test annotation processor output folders imported properly`() {
// default location for processor output when building by IDEA
val ideaGeneratedDir = "generated"
createProjectSubFile("src/main/$ideaGeneratedDir/Generated.java",
"public class Generated {}");
// default location for processor output when building by Gradle
val gradleGeneratedDir = "build/generated/sources/annotationProcessor/java/main"
createProjectSubFile("$gradleGeneratedDir/Generated.java",
"public class Generated {}");
val config = GradleBuildScriptBuilderEx()
.withJavaPlugin()
.withMavenCentral()
.addPostfix(
"""
dependencies {
annotationProcessor 'org.projectlombok:lombok:1.18.8'
}
""".trimIndent()).generate()
// import with default settings: delegate build to gradle
importProject(config);
assertSources("project.main", gradleGeneratedDir)
assertGeneratedSources("project.main", gradleGeneratedDir)
currentExternalProjectSettings.delegatedBuild = false;
// import with build by intellij idea
importProject(config);
assertSources("project.main", ideaGeneratedDir)
assertGeneratedSources("project.main", ideaGeneratedDir)
// switch delegation to gradle
currentExternalProjectSettings.delegatedBuild = true
GradleSettings.getInstance(myProject).publisher.onBuildDelegationChange(true, projectPath)
assertSources("project.main", gradleGeneratedDir)
assertGeneratedSources("project.main", gradleGeneratedDir)
// switch delegation to idea
currentExternalProjectSettings.delegatedBuild = false
GradleSettings.getInstance(myProject).publisher.onBuildDelegationChange(false, projectPath)
assertSources("project.main", ideaGeneratedDir)
assertGeneratedSources("project.main", ideaGeneratedDir)
}
@Test
@TargetVersions("4.6+")
fun `test two different annotation processors`() {
@@ -240,40 +240,6 @@ public class GradleFoldersImportingTest extends GradleImportingTestCase {
assertGeneratedTestSources("project", "src/test/java");
}
@Test
@TargetVersions("5.2+")
public void testGeneratedSourcesOutput() throws Exception {
createDefaultDirs();
createProjectSubFile("settings.gradle", "include('processor')");
createProjectSubFile("processor/build.gradle", "apply plugin:'java'");
createProjectSubFile("build/generated/sources/annotationProcessor/java/main/Generated.java");
importProject("" +
"apply plugin: 'java'\n" +
"dependencies {\n" +
" annotationProcessor project('processor')\n" +
"}");
assertSources("project.main",
"build/generated/sources/annotationProcessor/java/main",
"src/main/java");
assertGeneratedSources("project.main", "build/generated/sources/annotationProcessor/java/main");
}
@Test
@TargetVersions("5.2+")
public void testMissingAnnotationProcessor() throws Exception {
createDefaultDirs();
createProjectSubFile("settings.gradle", "include('processor')");
createProjectSubFile("processor/build.gradle", "apply plugin:'java'");
importProject("" +
"apply plugin: 'java'\n" +
"dependencies {\n" +
" annotationProcessor 'not.exist:processor:1.0'\n" +
"}");
assertSources("project.main",
"java");
assertGeneratedSources("project.main");
}
@Test
public void testCustomSourceSetsAreImported() throws Exception {
createDefaultDirs();
@@ -2,6 +2,7 @@
package org.jetbrains.plugins.gradle.model;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
@@ -20,4 +21,17 @@ public interface AnnotationProcessingConfig {
*/
@NotNull Collection<String> getAnnotationProcessorArguments();
/**
* If annotation processing is performed over test sources
* @return see above
*/
boolean isTestSources();
/**
* Location of files generated by annotation processing.
* Will be used as sources root, if needed
* @return see above
*/
@Nullable
String getProcessorOutput();
}
@@ -25,7 +25,8 @@ import java.util.*;
public class AnnotationProcessingModelBuilder extends AbstractModelBuilderService {
private static final boolean isAtLeastGradle3_4 = GradleVersion.current().compareTo(GradleVersion.version("3.4")) >= 0;
private static final boolean isAtLeastGradle4_5 = isAtLeastGradle3_4 && GradleVersion.current().compareTo(GradleVersion.version("4.5")) >= 0;
private static final boolean isAtLeastGradle4_3 = isAtLeastGradle3_4 && GradleVersion.current().compareTo(GradleVersion.version("4.3")) >= 0;
private static final boolean isAtLeastGradle4_5 = isAtLeastGradle4_3 && GradleVersion.current().compareTo(GradleVersion.version("4.5")) >= 0;
@Override
public Object buildAll(@NotNull String modelName, @NotNull Project project, @NotNull ModelBuilderContext context) {
@@ -64,7 +65,11 @@ public class AnnotationProcessingModelBuilder extends AbstractModelBuilderServic
for (File file : files) {
paths.add(file.getAbsolutePath());
}
sourceSetConfigs.put(sourceSet.getName(), new AnnotationProcessingConfigImpl(paths, annotationProcessorArgs));
File generatedSourcesDirectory = isAtLeastGradle4_3 ? options.getAnnotationProcessorGeneratedSourcesDirectory() : null;
String output = generatedSourcesDirectory != null ? generatedSourcesDirectory.getAbsolutePath() : null;
boolean isTest = SourceSet.TEST_SOURCE_SET_NAME.equals(sourceSet.getName());
sourceSetConfigs.put(sourceSet.getName(), new AnnotationProcessingConfigImpl(paths, annotationProcessorArgs, output, isTest));
}
}
}
@@ -306,15 +306,9 @@ class ExternalProjectBuilderImpl extends AbstractModelBuilderService {
DefaultExternalSourceDirectorySet generatedDirectorySet = null
def hasExplicitlyDefinedGeneratedSources = generatedSourceDirs && !generatedSourceDirs.isEmpty()
FileCollection generatedSourcesOutput = sourceSet.output.hasProperty("generatedSourcesDirs") ?
sourceSet.output.generatedSourcesDirs : null
def hasAnnotationProcessorClasspath = sourceSet.hasProperty("annotationProcessorPath") && !isEmpty(sourceSet.annotationProcessorPath)
if (hasExplicitlyDefinedGeneratedSources || hasAnnotationProcessorClasspath) {
if (hasExplicitlyDefinedGeneratedSources) {
def files = new HashSet<File>()
if (hasAnnotationProcessorClasspath && generatedSourcesOutput != null) {
files.addAll(generatedSourcesOutput.files)
}
for (File file : generatedSourceDirs) {
if (javaDirectorySet.srcDirs.contains(file)) {
files.add(file)
@@ -3,6 +3,7 @@ package org.jetbrains.plugins.gradle.tooling.internal;
import org.gradle.internal.impldep.com.google.common.base.Objects;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.model.AnnotationProcessingConfig;
import java.io.Serializable;
@@ -13,8 +14,12 @@ import java.util.Set;
public class AnnotationProcessingConfigImpl implements AnnotationProcessingConfig, Serializable {
private final Set<String> myPaths;
private final List<String> myArgs;
private final String myProcessorOutput;
private final boolean isTestSources;
public AnnotationProcessingConfigImpl(Set<String> files, List<String> args) {
public AnnotationProcessingConfigImpl(Set<String> files, List<String> args, String output, boolean sources) {
myProcessorOutput = output;
isTestSources = sources;
myPaths = files;
myArgs = args;
}
@@ -30,18 +35,30 @@ public class AnnotationProcessingConfigImpl implements AnnotationProcessingConfi
public Collection<String> getAnnotationProcessorArguments() {
return myArgs;
}
@Override
public boolean isTestSources() {
return isTestSources;
}
@Nullable
@Override
public String getProcessorOutput() {
return myProcessorOutput;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AnnotationProcessingConfigImpl config = (AnnotationProcessingConfigImpl)o;
return Objects.equal(myPaths, config.myPaths) &&
return myProcessorOutput == config.myProcessorOutput &&
isTestSources == config.isTestSources &&
Objects.equal(myPaths, config.myPaths) &&
Objects.equal(myArgs, config.myArgs);
}
@Override
public int hashCode() {
return Objects.hashCode(myPaths, myArgs);
return Objects.hashCode(myPaths, myArgs, myProcessorOutput, isTestSources);
}
}
@@ -16,7 +16,6 @@ import org.jetbrains.plugins.gradle.tooling.util.IntObjectMap;
import org.jetbrains.plugins.gradle.tooling.util.ObjectCollector;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@@ -97,6 +96,8 @@ public class AnnotationProcessingModelSerializationService implements Serializat
if (isAdded) {
writeStrings(writer, "args", config.getAnnotationProcessorArguments());
writeStrings(writer, "paths", config.getAnnotationProcessorPath());
writeString(writer, "output", config.getProcessorOutput());
writeBoolean(writer, "isTestSources", config.isTestSources());
}
writer.stepOut();
}
@@ -144,7 +145,9 @@ public class AnnotationProcessingModelSerializationService implements Serializat
public AnnotationProcessingConfigImpl create() {
List<String> args = readStringList(reader);
Set<String> files = readStringSet(reader);
return new AnnotationProcessingConfigImpl(files, args);
String output = readString(reader, "output");
boolean isTest = readBoolean(reader,"isTestSources");
return new AnnotationProcessingConfigImpl(files, args, output, isTest);
}
});
reader.stepOut();