diff --git a/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinWithLibraryConfigurator.kt b/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinWithLibraryConfigurator.kt index fd803ac4185..87a2a14de16 100644 --- a/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinWithLibraryConfigurator.kt +++ b/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinWithLibraryConfigurator.kt @@ -17,17 +17,21 @@ package org.jetbrains.kotlin.idea.configuration import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.JavaSdkVersion import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.* import com.intellij.openapi.roots.libraries.* +import com.intellij.openapi.util.Computable import com.intellij.openapi.vfs.JarFileSystem import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.psi.PsiElement +import com.intellij.psi.PsiJavaModule import org.jetbrains.annotations.Contract import org.jetbrains.kotlin.config.ApiVersion import org.jetbrains.kotlin.config.KotlinFacetSettingsProvider @@ -37,11 +41,16 @@ import org.jetbrains.kotlin.idea.KotlinPluginUtil import org.jetbrains.kotlin.idea.facet.getRuntimeLibraryVersion import org.jetbrains.kotlin.idea.framework.ui.CreateLibraryDialogWithModules import org.jetbrains.kotlin.idea.framework.ui.FileUIUtils +import org.jetbrains.kotlin.idea.quickfix.KotlinAddRequiredModuleFix import org.jetbrains.kotlin.idea.quickfix.askUpdateRuntime import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.idea.util.findFirstPsiJavaModule +import org.jetbrains.kotlin.idea.util.projectStructure.sdk +import org.jetbrains.kotlin.idea.util.projectStructure.version import org.jetbrains.kotlin.idea.versions.LibraryJarDescriptor import org.jetbrains.kotlin.idea.versions.findAllUsedLibraries import org.jetbrains.kotlin.idea.versions.findKotlinRuntimeLibrary +import org.jetbrains.kotlin.resolve.jvm.modules.KOTLIN_STDLIB_MODULE_NAME import java.io.File import java.util.* @@ -56,7 +65,7 @@ abstract class KotlinWithLibraryConfigurator internal constructor() : KotlinProj open val libraryType: LibraryType? = null - protected val libraryKind: PersistentLibraryKind<*>? = libraryType?.kind + protected val libraryKind: PersistentLibraryKind<*>? = libraryType?.kind override fun getStatus(moduleSourceRootGroup: ModuleSourceRootGroup): ConfigureKotlinStatus { val module = moduleSourceRootGroup.baseModule @@ -129,12 +138,12 @@ abstract class KotlinWithLibraryConfigurator internal constructor() : KotlinProj pathFromDialog: String?, collector: NotificationMessageCollector ) { - val classesPath = getPathToCopyFileTo(module.project, OrderRootType.CLASSES, defaultPath, pathFromDialog) - val sourcesPath = getPathToCopyFileTo(module.project, OrderRootType.SOURCES, defaultPath, pathFromDialog) + val classesPath = getPathToCopyFileTo(module.project, OrderRootType.CLASSES, defaultPath, pathFromDialog) + val sourcesPath = getPathToCopyFileTo(module.project, OrderRootType.SOURCES, defaultPath, pathFromDialog) configureModuleWithLibrary(module, classesPath, sourcesPath, collector, useBundled = pathFromDialog == null) } - fun configureModuleWithLibrary( + fun configureModuleWithLibrary( module: Module, classesPath: String, sourcesPath: String, @@ -149,7 +158,7 @@ abstract class KotlinWithLibraryConfigurator internal constructor() : KotlinProj ?: getKotlinLibrary(project) ?: createNewLibrary(project, collector) - val sdk = ModuleRootManager.getInstance(module).sdk + val sdk = module.sdk val model = library.modifiableModel for (descriptor in getLibraryJarDescriptors(sdk)) { @@ -167,6 +176,23 @@ abstract class KotlinWithLibraryConfigurator internal constructor() : KotlinProj ApplicationManager.getApplication().runWriteAction { model.commit() } addLibraryToModuleIfNeeded(module, library, collector) + + updateModuleInfo(module, collector) + } + + private fun updateModuleInfo(module: Module, collector: NotificationMessageCollector) { + if (module.sdk?.version?.isAtLeast(JavaSdkVersion.JDK_1_9) != true) return + + val project = module.project + val javaModule: PsiJavaModule = findFirstPsiJavaModule(module) ?: return + + val success = WriteCommandAction.runWriteCommandAction(project, Computable { + KotlinAddRequiredModuleFix.addModuleRequirement(javaModule, KOTLIN_STDLIB_MODULE_NAME) + }) + + if (success) { + collector.addMessage("Added $KOTLIN_STDLIB_MODULE_NAME requirement to module-info in ${module.name}") + } } fun configureLibraryJar( diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/KotlinAddRequiredModuleFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/KotlinAddRequiredModuleFix.kt index 85a6d107b8f..1e02b40f239 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/KotlinAddRequiredModuleFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/KotlinAddRequiredModuleFix.kt @@ -16,15 +16,38 @@ package org.jetbrains.kotlin.idea.quickfix +import com.intellij.codeInsight.daemon.QuickFixBundle import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil import com.intellij.codeInsight.daemon.impl.quickfix.AddRequiredModuleFix import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.* +import com.intellij.psi.util.PsiUtil +import com.intellij.util.containers.ContainerUtil import org.jetbrains.kotlin.diagnostics.Diagnostic import org.jetbrains.kotlin.diagnostics.DiagnosticFactory +import org.jetbrains.kotlin.idea.util.findRequireDirective import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.resolve.jvm.diagnostics.ErrorsJvm -class KotlinAddRequiredModuleFix { +class KotlinAddRequiredModuleFix(module: PsiJavaModule, private val requiredName: String) : LocalQuickFixAndIntentionActionOnPsiElement(module) { + override fun getFamilyName(): String = QuickFixBundle.message("module.info.add.requires.family.name") + override fun getText(): String = QuickFixBundle.message("module.info.add.requires.name", requiredName); + override fun startInWriteAction() = true + + override fun isAvailable(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement): Boolean { + return PsiUtil.isLanguageLevel9OrHigher(file) && + startElement is PsiJavaModule && + startElement.getManager().isInProject(startElement) && + getLBrace(startElement) != null; + } + + override fun invoke(project: Project, file: PsiFile, editor: Editor?, startElement: PsiElement, endElement: PsiElement) { + addModuleRequirement(startElement as PsiJavaModule, requiredName) + } + companion object : KotlinSingleIntentionActionFactory() { override fun createAction(diagnostic: Diagnostic): IntentionAction? { val expression = diagnostic.psiElement as? KtExpression ?: return null @@ -33,7 +56,38 @@ class KotlinAddRequiredModuleFix { val dependDiagnostic = DiagnosticFactory.cast(diagnostic, ErrorsJvm.JAVA_MODULE_DOES_NOT_DEPEND_ON_MODULE) val moduleName = dependDiagnostic.a - return AddRequiredModuleFix(javaModule, moduleName) + return KotlinAddRequiredModuleFix(javaModule, moduleName) + } + + fun addModuleRequirement(module: PsiJavaModule, requiredName: String): Boolean { + if (!module.isValid) return false + if (findRequireDirective(module, requiredName) != null) return false + + val parserFacade = JavaPsiFacade.getInstance(module.project).parserFacade + val tempModule = parserFacade.createModuleFromText("module TempModuleName { requires $requiredName; }") + val requiresStatement = tempModule.requires.first() + + val addingPlace = findAddingPlace(module) ?: return false + addingPlace.parent.addAfter(requiresStatement, addingPlace) + + return true + } + + private fun getLBrace(module: PsiJavaModule): PsiElement? { + val nameElement = module.nameIdentifier + var element: PsiElement? = nameElement.nextSibling + while (element != null) { + if (PsiUtil.isJavaToken(element, JavaTokenType.LBRACE)) { + return element + } + element = element.nextSibling + } + return null // module-info is incomplete + } + + private fun findAddingPlace(module: PsiJavaModule): PsiElement? { + val addingPlace = module.requires.lastOrNull() + return addingPlace ?: getLBrace(module) } } } \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/util/java9StructureUtil.kt b/idea/src/org/jetbrains/kotlin/idea/util/java9StructureUtil.kt new file mode 100644 index 00000000000..df601cab301 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/util/java9StructureUtil.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * 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 + * + * http://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.jetbrains.kotlin.idea.util + +import com.intellij.openapi.module.Module +import com.intellij.psi.PsiJavaFile +import com.intellij.psi.PsiJavaModule +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiRequiresStatement +import com.intellij.psi.search.FilenameIndex + +fun findFirstPsiJavaModule(module: Module): PsiJavaModule? { + val project = module.project + + val moduleInfoFiles = FilenameIndex.getVirtualFilesByName(project, PsiJavaModule.MODULE_INFO_FILE, module.moduleScope) + return moduleInfoFiles + .asSequence() + .map(PsiManager.getInstance(project)::findFile) + .filterIsInstance() + .map { it.moduleDeclaration } + .firstOrNull { it != null } +} + +fun findRequireDirective(module: PsiJavaModule, requiredName: String): PsiRequiresStatement? = + module.requires.find { it.moduleName == requiredName } diff --git a/idea/testData/configuration/java9WithModuleInfo/module.iml b/idea/testData/configuration/java9WithModuleInfo/module.iml new file mode 100644 index 00000000000..c90834f2d60 --- /dev/null +++ b/idea/testData/configuration/java9WithModuleInfo/module.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/idea/testData/configuration/java9WithModuleInfo/projectFile.ipr b/idea/testData/configuration/java9WithModuleInfo/projectFile.ipr new file mode 100644 index 00000000000..10606d0eb8c --- /dev/null +++ b/idea/testData/configuration/java9WithModuleInfo/projectFile.ipr @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/idea/testData/configuration/java9WithModuleInfo/src/module-info.java b/idea/testData/configuration/java9WithModuleInfo/src/module-info.java new file mode 100644 index 00000000000..10e4b444089 --- /dev/null +++ b/idea/testData/configuration/java9WithModuleInfo/src/module-info.java @@ -0,0 +1,3 @@ +module MAIN { + +} \ No newline at end of file diff --git a/idea/testData/configuration/java9WithModuleInfoWithStdlibAlready/module.iml b/idea/testData/configuration/java9WithModuleInfoWithStdlibAlready/module.iml new file mode 100644 index 00000000000..c90834f2d60 --- /dev/null +++ b/idea/testData/configuration/java9WithModuleInfoWithStdlibAlready/module.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/idea/testData/configuration/java9WithModuleInfoWithStdlibAlready/projectFile.ipr b/idea/testData/configuration/java9WithModuleInfoWithStdlibAlready/projectFile.ipr new file mode 100644 index 00000000000..10606d0eb8c --- /dev/null +++ b/idea/testData/configuration/java9WithModuleInfoWithStdlibAlready/projectFile.ipr @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/idea/testData/configuration/java9WithModuleInfoWithStdlibAlready/src/module-info.java b/idea/testData/configuration/java9WithModuleInfoWithStdlibAlready/src/module-info.java new file mode 100644 index 00000000000..975b4781323 --- /dev/null +++ b/idea/testData/configuration/java9WithModuleInfoWithStdlibAlready/src/module-info.java @@ -0,0 +1,3 @@ +module MAIN { + requires kotlin.stdlib; +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/configuration/ConfigureKotlinTest.java b/idea/tests/org/jetbrains/kotlin/idea/configuration/ConfigureKotlinTest.java index c52038dfb12..490b8b7cdcf 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/configuration/ConfigureKotlinTest.java +++ b/idea/tests/org/jetbrains/kotlin/idea/configuration/ConfigureKotlinTest.java @@ -19,11 +19,14 @@ package org.jetbrains.kotlin.idea.configuration; import com.intellij.openapi.externalSystem.service.project.IdeModifiableModelsProviderImpl; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.LibraryOrderEntry; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.roots.RootPolicy; import com.intellij.openapi.roots.libraries.Library; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.psi.PsiJavaModule; +import com.intellij.psi.PsiRequiresStatement; import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments; import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments; import org.jetbrains.kotlin.config.*; @@ -31,11 +34,14 @@ import org.jetbrains.kotlin.idea.facet.FacetUtilsKt; import org.jetbrains.kotlin.idea.facet.KotlinFacet; import org.jetbrains.kotlin.idea.framework.JsLibraryStdDetectionUtil; import org.jetbrains.kotlin.idea.project.PlatformKt; +import org.jetbrains.kotlin.idea.util.Java9StructureUtilKt; import org.jetbrains.kotlin.idea.versions.KotlinRuntimeLibraryUtilKt; +import org.jetbrains.kotlin.resolve.jvm.modules.JavaModuleKt; import java.io.File; import java.io.IOException; import java.util.Collections; +import java.util.stream.StreamSupport; public class ConfigureKotlinTest extends AbstractConfigureKotlinTest { public void testNewLibrary_copyJar() { @@ -250,6 +256,36 @@ public class ConfigureKotlinTest extends AbstractConfigureKotlinTest { assertEquals(KotlinFacet.Companion.get(module2).getConfiguration().getSettings().getImplementedModuleName(), "module1"); } + public void testJava9WithModuleInfo() { + checkAddStdlibModule(); + } + + public void testJava9WithModuleInfoWithStdlibAlready() { + checkAddStdlibModule(); + } + + private void checkAddStdlibModule() { + doTestOneJavaModule(KotlinWithLibraryConfigurator.FileState.COPY); + + Module module = getModule(); + Sdk moduleSdk = ModuleRootManager.getInstance(getModule()).getSdk(); + assertNotNull("Module SDK is not defined", moduleSdk); + + PsiJavaModule javaModule = Java9StructureUtilKt.findFirstPsiJavaModule(module); + assertNotNull(javaModule); + + PsiRequiresStatement stdlibDirective = + Java9StructureUtilKt.findRequireDirective(javaModule, JavaModuleKt.KOTLIN_STDLIB_MODULE_NAME); + assertNotNull("Require directive for " + JavaModuleKt.KOTLIN_STDLIB_MODULE_NAME + " is expected", + stdlibDirective); + + long numberOfStdlib = StreamSupport.stream(javaModule.getRequires().spliterator(), false) + .filter((statement) -> JavaModuleKt.KOTLIN_STDLIB_MODULE_NAME.equals(statement.getModuleName())) + .count(); + + assertTrue("Only one standard library directive is expected", numberOfStdlib == 1); + } + private void configureFacetAndCheckJvm(JvmTarget jvmTarget) { IdeModifiableModelsProviderImpl modelsProvider = new IdeModifiableModelsProviderImpl(getProject()); try {