diff --git a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index da390c442ac..391584a8812 100644 --- a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -85,6 +85,7 @@ import org.jetbrains.kotlin.idea.intentions.declarations.AbstractJoinLinesTest import org.jetbrains.kotlin.idea.internal.AbstractBytecodeToolWindowTest import org.jetbrains.kotlin.idea.kdoc.AbstractKDocHighlightingTest import org.jetbrains.kotlin.idea.kdoc.AbstractKDocTypingTest +import org.jetbrains.kotlin.idea.maven.AbstractKotlinMavenInspectionTest import org.jetbrains.kotlin.idea.navigation.AbstractGotoSuperTest import org.jetbrains.kotlin.idea.navigation.AbstractKotlinGotoImplementationTest import org.jetbrains.kotlin.idea.navigation.AbstractKotlinGotoTest @@ -490,6 +491,10 @@ fun main(args: Array) { model("inspections", pattern = "^(inspections\\.test)$", singleClass = true) } + testClass { + model("maven-inspections", pattern = "^([\\w\\-]+).xml$", singleClass = true) + } + testClass() { model("hierarchy/class/type", extension = null, recursive = false, testMethod = "doTypeClassHierarchyTest") model("hierarchy/class/super", extension = null, recursive = false, testMethod = "doSuperClassHierarchyTest") diff --git a/idea/resources/intentionDescriptions/MavenPluginSourcesMoveToBuild/description.html b/idea/resources/intentionDescriptions/MavenPluginSourcesMoveToBuild/description.html new file mode 100644 index 00000000000..a0d58e87d94 --- /dev/null +++ b/idea/resources/intentionDescriptions/MavenPluginSourcesMoveToBuild/description.html @@ -0,0 +1,5 @@ + + +This intention moves source directory from execution's configuration to build the corresponding source directory tag + + \ No newline at end of file diff --git a/idea/resources/intentionDescriptions/MavenPluginSourcesMoveToExecutionIntention/description.html b/idea/resources/intentionDescriptions/MavenPluginSourcesMoveToExecutionIntention/description.html new file mode 100644 index 00000000000..2d7bf675eb1 --- /dev/null +++ b/idea/resources/intentionDescriptions/MavenPluginSourcesMoveToExecutionIntention/description.html @@ -0,0 +1,6 @@ + + +This intention moves source directory from build source directory tag +to the corresponding kotlin-maven-plugin execution's configuration + + \ No newline at end of file diff --git a/idea/src/META-INF/maven.xml b/idea/src/META-INF/maven.xml index a3da2212a9c..1a9911c2fb5 100644 --- a/idea/src/META-INF/maven.xml +++ b/idea/src/META-INF/maven.xml @@ -7,9 +7,51 @@ + + + + + org.jetbrains.kotlin.idea.configuration.MavenPluginSourcesMoveToBuild + Kotlin + + + org.jetbrains.kotlin.idea.configuration.MavenPluginSourcesMoveToExecutionIntention + Kotlin + + + org.jetbrains.kotlin.idea.configuration.KotlinImporterComponent + + + + + + + + + + + + + + diff --git a/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinJavaMavenConfigurator.java b/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinJavaMavenConfigurator.java index 52b17d3f9d9..aa476514db6 100644 --- a/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinJavaMavenConfigurator.java +++ b/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinJavaMavenConfigurator.java @@ -24,7 +24,7 @@ import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform; public class KotlinJavaMavenConfigurator extends KotlinMavenConfigurator { private static final String NAME = "maven"; - private static final String STD_LIB_ID = "kotlin-stdlib"; + public static final String STD_LIB_ID = "kotlin-stdlib"; private static final String TEST_LIB_ID = "kotlin-test-junit"; private static final String PRESENTABLE_TEXT = "Maven"; @@ -43,6 +43,12 @@ public class KotlinJavaMavenConfigurator extends KotlinMavenConfigurator { createExecution(pomFile, kotlinPlugin, module, true); } + @NotNull + @Override + protected String getGoal(boolean isTest) { + return isTest ? PomFile.KotlinGoals.INSTANCE.getTestCompile() : PomFile.KotlinGoals.INSTANCE.getCompile(); + } + @NotNull @Override public TargetPlatform getTargetPlatform() { diff --git a/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinJavascriptMavenConfigurator.java b/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinJavascriptMavenConfigurator.java index 92e113e1599..f4d63d413fa 100644 --- a/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinJavascriptMavenConfigurator.java +++ b/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinJavascriptMavenConfigurator.java @@ -24,7 +24,7 @@ import org.jetbrains.kotlin.resolve.TargetPlatform; public class KotlinJavascriptMavenConfigurator extends KotlinMavenConfigurator { private static final String NAME = "js maven"; - private static final String STD_LIB_ID = "kotlin-js-library"; + public static final String STD_LIB_ID = "kotlin-js-library"; private static final String JS_GOAL = "js"; private static final String JS_TEST_GOAL = "test-js"; private static final String JS_EXECUTION_ID = "js"; diff --git a/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinMavenConfigurator.java b/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinMavenConfigurator.java index ce171dce020..1ed2253274b 100644 --- a/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinMavenConfigurator.java +++ b/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinMavenConfigurator.java @@ -24,11 +24,7 @@ import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; -import com.intellij.openapi.roots.ContentEntry; -import com.intellij.openapi.roots.ModuleRootManager; -import com.intellij.openapi.roots.SourceFolder; import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.WritingAccessProvider; import com.intellij.psi.PsiFile; @@ -39,15 +35,14 @@ import com.intellij.psi.xml.XmlFile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.maven.dom.MavenDomUtil; -import org.jetbrains.idea.maven.dom.model.*; +import org.jetbrains.idea.maven.dom.model.MavenDomPlugin; +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel; import org.jetbrains.idea.maven.model.MavenId; import org.jetbrains.idea.maven.project.MavenProjectsManager; import org.jetbrains.idea.maven.utils.MavenArtifactScope; -import org.jetbrains.jps.model.java.JavaSourceRootType; import org.jetbrains.kotlin.idea.KotlinPluginUtil; import org.jetbrains.kotlin.idea.framework.ui.ConfigureDialogWithModulesAndVersion; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -55,16 +50,10 @@ import java.util.List; public abstract class KotlinMavenConfigurator implements KotlinProjectConfigurator { public static final String NAME = "maven"; - private static final String GROUP_ID = "org.jetbrains.kotlin"; - private static final String MAVEN_PLUGIN_ID = "kotlin-maven-plugin"; + public static final String GROUP_ID = "org.jetbrains.kotlin"; + public static final String MAVEN_PLUGIN_ID = "kotlin-maven-plugin"; private static final String KOTLIN_VERSION_PROPERTY = "kotlin.version"; - private static final String PROCESS_TEST_SOURCES_PHASE = "process-test-sources"; - private static final String PROCESS_SOURCES_PHASE = "process-sources"; - private static final String TEST_COMPILE_PHASE = "test-compile"; - private static final String COMPILE_PHASE = "compile"; - private static final String TEST_COMPILE_GOAL = "test-compile"; - private static final String COMPILE_GOAL = "compile"; private static final String TEST_COMPILE_EXECUTION_ID = "test-compile"; private static final String COMPILE_EXECUTION_ID = "compile"; @@ -146,9 +135,7 @@ public abstract class KotlinMavenConfigurator implements KotlinProjectConfigurat protected abstract void createExecutions(@NotNull PomFile pomFile, @NotNull MavenDomPlugin kotlinPlugin, @NotNull Module module); @NotNull - protected String getGoal(boolean isTest) { - return isTest ? TEST_COMPILE_GOAL : COMPILE_GOAL; - } + protected abstract String getGoal(boolean isTest); @NotNull protected String getExecutionId(boolean isTest) { @@ -208,36 +195,8 @@ public abstract class KotlinMavenConfigurator implements KotlinProjectConfigurat @NotNull Module module, boolean isTest ) { - MavenDomPluginExecution execution = pomFile.addExecution(kotlinPlugin, getExecutionId(isTest), getPhase(module, isTest), Collections.singletonList(getGoal(isTest))); - - List sourceDirs = new ArrayList(); - for (ContentEntry contentEntry : ModuleRootManager.getInstance(module).getContentEntries()) { - SourceFolder[] folders = contentEntry.getSourceFolders(); - for (SourceFolder sourceFolder : folders) { - if (isRelatedSourceRoot(isTest, sourceFolder)) { - VirtualFile sourceFolderFile = sourceFolder.getFile(); - if (sourceFolderFile != null) { - String relativePath = VfsUtilCore.getRelativePath(sourceFolderFile, pomFile.getXmlFile().getVirtualFile().getParent(), '/'); - sourceDirs.add(relativePath); - } - } - } - } - - pomFile.executionSourceDirs(execution, sourceDirs); - } - - private static boolean isRelatedSourceRoot(boolean isTest, SourceFolder folder) { - return isTest && folder.getRootType() == JavaSourceRootType.TEST_SOURCE || - (!isTest && folder.getRootType() == JavaSourceRootType.SOURCE); - } - - @NotNull - private static String getPhase(@NotNull Module module, boolean isTest) { - if (hasJavaFiles(module)) { - return isTest ? PROCESS_TEST_SOURCES_PHASE : PROCESS_SOURCES_PHASE; - } - return isTest ? TEST_COMPILE_PHASE : COMPILE_PHASE; + pomFile.addKotlinExecution(module, kotlinPlugin, getExecutionId(isTest), PomFile.Companion.getPhase(hasJavaFiles(module), isTest), isTest, + Collections.singletonList(getGoal(isTest))); } private static boolean hasJavaFiles(@NotNull Module module) { diff --git a/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinMavenPluginPhaseInspection.kt b/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinMavenPluginPhaseInspection.kt new file mode 100644 index 00000000000..5631509f3df --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/configuration/KotlinMavenPluginPhaseInspection.kt @@ -0,0 +1,200 @@ +/* + * Copyright 2010-2016 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.configuration + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.ide.highlighter.JavaFileType +import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.psi.search.FileTypeIndex +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.xml.XmlFile +import com.intellij.util.xml.DomFileElement +import com.intellij.util.xml.highlighting.DomElementAnnotationHolder +import com.intellij.util.xml.highlighting.DomElementsInspection +import org.jetbrains.idea.maven.dom.model.MavenDomGoal +import org.jetbrains.idea.maven.dom.model.MavenDomPlugin +import org.jetbrains.idea.maven.dom.model.MavenDomPluginExecution +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel +import org.jetbrains.idea.maven.model.MavenId +import org.jetbrains.idea.maven.model.MavenPlugin +import org.jetbrains.idea.maven.project.MavenProjectsManager +import org.jetbrains.idea.maven.utils.MavenArtifactScope +import org.jetbrains.kotlin.idea.KotlinFileType +import java.util.* + +class KotlinMavenPluginPhaseInspection : DomElementsInspection(MavenDomProjectModel::class.java) { + override fun getStaticDescription() = "The inspecition's purpose is to check Maven pom and kotlin maven plugin configuration" + + override fun checkFileElement(domFileElement: DomFileElement?, holder: DomElementAnnotationHolder?) { + if (domFileElement == null || holder == null) { + return + } + + val module = domFileElement.module ?: return + val manager = MavenProjectsManager.getInstance(module.project) + val mavenProject = manager.findProject(module) ?: return + + val pom = PomFile(domFileElement.file) + val hasJavaFiles = module.hasJavaFiles() + + // all executions including inherited + val executions = mavenProject.plugins + .filter { it.isKotlinMavenPlugin() } + .flatMap { it.executions } + val allGoalsSet: Set = executions.flatMapTo(HashSet()) { it.goals } + val hasJvmExecution = PomFile.KotlinGoals.Compile in allGoalsSet || PomFile.KotlinGoals.TestCompile in allGoalsSet + val hasJsExecution = PomFile.KotlinGoals.Js in allGoalsSet || PomFile.KotlinGoals.TestJs in allGoalsSet + + val pomKotlinPlugins = pom.findKotlinPlugins() + + for (kotlinPlugin in pomKotlinPlugins) { + if (PomFile.KotlinGoals.Compile !in allGoalsSet && PomFile.KotlinGoals.Js !in allGoalsSet) { + val fixes = if (hasJavaFiles) { + arrayOf(AddExecutionLocalFix(domFileElement.file, module, kotlinPlugin, PomFile.KotlinGoals.Compile)) + } + else { + arrayOf(AddExecutionLocalFix(domFileElement.file, module, kotlinPlugin, PomFile.KotlinGoals.Compile), + AddExecutionLocalFix(domFileElement.file, module, kotlinPlugin, PomFile.KotlinGoals.Js)) + } + + holder.createProblem(kotlinPlugin.artifactId.createStableCopy(), + HighlightSeverity.WARNING, + "Kotlin plugin has no compile executions", + *fixes) + } + else { + if (hasJavaFiles) { + pom.findExecutions(kotlinPlugin, PomFile.KotlinGoals.Compile).notAtPhase(PomFile.DefaultPhases.ProcessSources).forEach { badExecution -> + holder.createProblem(badExecution.phase.createStableCopy(), + HighlightSeverity.WARNING, + "Kotlin plugin should run before javac so kotlin classes could be visible from Java", + FixExecutionPhaseLocalFix(badExecution, PomFile.DefaultPhases.ProcessSources)) + } + + pom.findExecutions(kotlinPlugin, PomFile.KotlinGoals.Js, PomFile.KotlinGoals.TestJs).forEach { badExecution -> + holder.createProblem(badExecution.goals.goals.first { it.isJsGoal() }.createStableCopy(), + HighlightSeverity.WARNING, + "JavaScript goal configured for module with Java files") + } + } + + val stdlibDependencies = mavenProject.findDependencies(KotlinJavaMavenConfigurator.GROUP_ID, KotlinJavaMavenConfigurator.STD_LIB_ID) + val jsDependencies = mavenProject.findDependencies(KotlinJavaMavenConfigurator.GROUP_ID, KotlinJavascriptMavenConfigurator.STD_LIB_ID) + + if (hasJvmExecution && stdlibDependencies.isEmpty()) { + holder.createProblem(kotlinPlugin.artifactId.createStableCopy(), + HighlightSeverity.WARNING, + "Kotlin JVM compiler configured but no ${KotlinJavaMavenConfigurator.STD_LIB_ID} dependency", + FixAddStdlibLocalFix(domFileElement.file, KotlinJavaMavenConfigurator.STD_LIB_ID, kotlinPlugin.version.rawText)) + } + if (hasJsExecution && jsDependencies.isEmpty()) { + holder.createProblem(kotlinPlugin.artifactId.createStableCopy(), + HighlightSeverity.WARNING, + "Kotlin JavaScript compiler configured but no ${KotlinJavascriptMavenConfigurator.STD_LIB_ID} dependency", + FixAddStdlibLocalFix(domFileElement.file, KotlinJavascriptMavenConfigurator.STD_LIB_ID, kotlinPlugin.version.rawText)) + } + } + } + + val stdlibDependencies = pom.findDependencies(MavenId(KotlinJavaMavenConfigurator.GROUP_ID, KotlinJavaMavenConfigurator.STD_LIB_ID, null)) + if (!hasJvmExecution && stdlibDependencies.isNotEmpty()) { + stdlibDependencies.forEach { dep -> + holder.createProblem(dep.artifactId.createStableCopy(), + HighlightSeverity.WARNING, + "You have ${dep.artifactId} configured but no corresponding plugin execution", + ConfigurePluginExecutionLocalFix(module, domFileElement.file, PomFile.KotlinGoals.Compile, dep.version.rawText)) + } + } + + val stdlibJsDependencies = pom.findDependencies(MavenId(KotlinJavaMavenConfigurator.GROUP_ID, KotlinJavascriptMavenConfigurator.STD_LIB_ID, null)) + if (!hasJsExecution && stdlibJsDependencies.isNotEmpty()) { + stdlibJsDependencies.forEach { dep -> + holder.createProblem(dep.artifactId.createStableCopy(), + HighlightSeverity.WARNING, + "You have ${dep.artifactId} configured but no corresponding plugin execution", + ConfigurePluginExecutionLocalFix(module, domFileElement.file, PomFile.KotlinGoals.Js, dep.version.rawText)) + } + } + + pom.findKotlinExecutions().filter { + it.goals.goals.any { it.rawText == PomFile.KotlinGoals.Compile || it.rawText == PomFile.KotlinGoals.Js } + && it.goals.goals.any { it.rawText == PomFile.KotlinGoals.TestCompile || it.rawText == PomFile.KotlinGoals.TestJs } + }.forEach { badExecution -> + holder.createProblem(badExecution.goals.createStableCopy(), + HighlightSeverity.WEAK_WARNING, + "It is not recommended to have both test and compile goals in the same execution") + } + } + + private class AddExecutionLocalFix(val file: XmlFile, val module: Module, val kotlinPlugin: MavenDomPlugin, val goal: String) : LocalQuickFix { + override fun getName() = "Create $goal execution" + + override fun getFamilyName() = "Kotlin" + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val pom = PomFile(file) + + pom.addKotlinExecution(module, kotlinPlugin, goal, PomFile.getPhase(module.hasJavaFiles(), false), false, listOf(goal)) + } + } + + private class FixExecutionPhaseLocalFix(val execution: MavenDomPluginExecution, val newPhase: String) : LocalQuickFix { + override fun getName() = "Change phase to $newPhase" + + override fun getFamilyName() = "Kotlin" + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + execution.phase.value = newPhase + } + } + + private class FixAddStdlibLocalFix(val pomFile: XmlFile, val id: String, val version: String?) : LocalQuickFix { + override fun getName() = "Add $id dependency" + override fun getFamilyName() = "Kotlin" + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val file = PomFile(pomFile) + file.addDependency(MavenId(KotlinJavaMavenConfigurator.GROUP_ID, id, version), MavenArtifactScope.COMPILE) + } + } + + private class ConfigurePluginExecutionLocalFix(val module: Module, val xmlFile: XmlFile, val goal: String, val version: String?) : LocalQuickFix { + override fun getName() = "Create $goal execution of kotlin-maven-compiler" + override fun getFamilyName() = "Kotlin" + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val pom = PomFile(xmlFile) + val plugin = pom.addKotlinPlugin(version) + pom.addKotlinExecution(module, plugin, "compile", PomFile.getPhase(module.hasJavaFiles(), false), false, listOf(goal)) + } + } +} + +fun Module.hasJavaFiles(): Boolean { + return FileTypeIndex.containsFileOfType(JavaFileType.INSTANCE, GlobalSearchScope.moduleScope(this)) +} + +private fun MavenPlugin.isKotlinMavenPlugin() = groupId == KotlinMavenConfigurator.GROUP_ID + && artifactId == KotlinMavenConfigurator.MAVEN_PLUGIN_ID + +private fun MavenDomGoal.isJsGoal() = rawText == PomFile.KotlinGoals.Js || rawText == PomFile.KotlinGoals.TestJs + +private fun List.atPhase(phase: String) = filter { it.phase.stringValue == phase } +private fun List.notAtPhase(phase: String) = filter { it.phase.stringValue != phase } diff --git a/idea/src/org/jetbrains/kotlin/idea/configuration/MavenActionsGroup.kt b/idea/src/org/jetbrains/kotlin/idea/configuration/MavenActionsGroup.kt new file mode 100644 index 00000000000..97f1037ae5e --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/configuration/MavenActionsGroup.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2010-2016 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.configuration + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.util.xml.DomElement +import com.intellij.util.xml.DomUtil +import com.intellij.util.xml.actions.generate.AbstractDomGenerateProvider +import com.intellij.util.xml.ui.actions.generate.GenerateDomElementAction +import org.jetbrains.idea.maven.dom.MavenDomUtil +import org.jetbrains.idea.maven.dom.model.MavenDomDependency +import org.jetbrains.idea.maven.dom.model.MavenDomPlugin +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel +import org.jetbrains.idea.maven.model.MavenId +import org.jetbrains.kotlin.psi.psiUtil.endOffset + +class GenerateMavenCompileExecutionAction : PomFileActionBase(KotlinMavenExecutionProvider("compile", PomFile.DefaultPhases.Compile)) +class GenerateMavenTestCompileExecutionAction : PomFileActionBase(KotlinMavenExecutionProvider("test-compile", PomFile.DefaultPhases.TestCompile)) +class GenerateMavenPluginAction : PomFileActionBase(KotlinMavenPluginProvider()) + +private val DefaultKotlinVersion = "\${kotlin.version}" + +open class PomFileActionBase(generateProvider: AbstractDomGenerateProvider<*>) : GenerateDomElementAction(generateProvider) { + override fun isValidForFile(project: Project, editor: Editor, file: PsiFile): Boolean { + return MavenDomUtil.isMavenFile(file) && super.isValidForFile(project, editor, file) + } + + override fun startInWriteAction() = true +} + +private class KotlinMavenPluginProvider : AbstractDomGenerateProvider("kotlin-maven-plugin-provider", MavenDomPlugin::class.java) { + + override fun generate(parent: DomElement?, editor: Editor?): MavenDomPlugin? { + if (parent !is MavenDomProjectModel) { + return null + } + + val knownVersion = parent.dependencies.dependencies.firstOrNull { it.isKotlinStdlib() }?.version?.rawText + val version = when { + knownVersion == null -> DefaultKotlinVersion + knownVersion.isRangeVersion() -> knownVersion.getRangeClosedEnd() ?: DefaultKotlinVersion + else -> knownVersion + } + + val pom = PomFile(DomUtil.getFile(parent)) + val plugin = pom.addPlugin(MavenId(KotlinMavenConfigurator.GROUP_ID, KotlinMavenConfigurator.MAVEN_PLUGIN_ID, version)) + val range = plugin.version.ensureTagExists().value.textRange + + if (editor != null) { + editor.caretModel.moveToOffset(range.endOffset) + editor.selectionModel.setSelection(range.startOffset, range.endOffset) + } + + return plugin + } + + override fun getElementToNavigate(t: MavenDomPlugin?) = null + + override fun getParentDomElement(project: Project?, editor: Editor?, file: PsiFile?): DomElement? { + if (project == null || editor == null || file == null) { + return null + } + + return DomUtil.getContextElement(editor)?.findProject() + } + + override fun isAvailableForElement(contextElement: DomElement): Boolean { + val parent = contextElement.findProject() ?: return false + + return parent.build.plugins.plugins.none { plugin -> plugin.isKotlinMavenPlugin() } + } +} + +private class KotlinMavenExecutionProvider(val goal: String, val phase: String) : AbstractDomGenerateProvider("kotlin-maven-execution-provider", MavenDomPlugin::class.java) { + + override fun generate(parent: DomElement?, editor: Editor?): MavenDomPlugin? { + if (parent !is MavenDomPlugin) { + return null + } + + val file = PomFile(DomUtil.getFile(parent)) + val execution = file.addExecution(parent, goal, phase, listOf(goal)) + + if (editor != null) { + editor.caretModel.moveToOffset(execution.ensureXmlElementExists().endOffset) + } + + return parent + } + + override fun getElementToNavigate(t: MavenDomPlugin?) = null + + override fun getParentDomElement(project: Project?, editor: Editor?, file: PsiFile?): DomElement? { + if (project == null || editor == null || file == null) { + return null + } + + return DomUtil.getContextElement(editor)?.findPlugin() + } + + override fun isAvailableForElement(contextElement: DomElement): Boolean { + val plugin = contextElement.findPlugin() + return plugin != null + && plugin.isKotlinMavenPlugin() + && plugin.executions.executions.none { it.goals.goals.any { it.rawText == goal } } + } + +} + +private fun String.getRangeClosedEnd(): String? = when { + startsWith("[") -> substringBefore(',', "").drop(1).trimEnd() + endsWith("]") -> substringAfterLast(',', "").dropLast(1).trimStart() + else -> null +} + +private fun Char.isRangeStart() = this == '[' || this == '(' +private fun Char.isRangeEnd() = this == ']' || this == ')' + +private fun String.isRangeVersion() = length > 2 && this[0].isRangeStart() && last().isRangeEnd() + +private fun DomElement.findProject(): MavenDomProjectModel? = this as? MavenDomProjectModel ?: DomUtil.getParentOfType(this, MavenDomProjectModel::class.java, true) +private fun DomElement.findPlugin(): MavenDomPlugin? = this as? MavenDomPlugin ?: DomUtil.getParentOfType(this, MavenDomPlugin::class.java, true) + +private fun MavenDomPlugin.isKotlinMavenPlugin() = groupId.stringValue == KotlinMavenConfigurator.GROUP_ID + && artifactId.stringValue == KotlinMavenConfigurator.MAVEN_PLUGIN_ID + +private fun MavenDomDependency.isKotlinStdlib() = groupId.stringValue == KotlinMavenConfigurator.GROUP_ID + && artifactId.stringValue == KotlinJavaMavenConfigurator.STD_LIB_ID diff --git a/idea/src/org/jetbrains/kotlin/idea/configuration/MavenPluginSourcesSwap.kt b/idea/src/org/jetbrains/kotlin/idea/configuration/MavenPluginSourcesSwap.kt new file mode 100644 index 00000000000..0f0c6e48ca1 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/configuration/MavenPluginSourcesSwap.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2010-2016 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.configuration + +import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.xml.XmlElement +import com.intellij.psi.xml.XmlFile +import com.intellij.psi.xml.XmlTag +import com.intellij.util.xml.DomManager +import com.intellij.util.xml.GenericDomValue +import org.jetbrains.idea.maven.dom.MavenDomUtil +import org.jetbrains.idea.maven.dom.model.MavenDomBuild +import org.jetbrains.idea.maven.dom.model.MavenDomPluginExecution +import org.jetbrains.kotlin.psi.psiUtil.getParentOfType +import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf + +class MavenPluginSourcesMoveToExecutionIntention : PsiElementBaseIntentionAction() { + override fun getFamilyName() = "Move to compile execution" + override fun getText() = familyName + + override fun isAvailable(project: Project, editor: Editor, element: PsiElement): Boolean { + val file = element.containingFile + + if (file == null || !MavenDomUtil.isMavenFile(file) || element !is XmlElement) { + return false + } + + val tag = element.getParentOfType(false) ?: return false + val domElement = DomManager.getDomManager(project).getDomElement(tag) ?: return false + + if (domElement !is GenericDomValue<*>) { + return false + } + + val pom = PomFile(file as XmlFile) + if (domElement.getParentOfType(MavenDomBuild::class.java, false)?.sourceDirectory === domElement) { + return pom.findKotlinExecutions(PomFile.KotlinGoals.Compile, PomFile.KotlinGoals.Js).isNotEmpty() + } + if (domElement.getParentOfType(MavenDomBuild::class.java, false)?.testSourceDirectory === domElement) { + return pom.findKotlinExecutions(PomFile.KotlinGoals.TestCompile, PomFile.KotlinGoals.TestJs).isNotEmpty() + } + + return false + } + + override fun startInWriteAction() = true + + override fun invoke(project: Project, editor: Editor, element: PsiElement) { + val xmlFile = element.containingFile as? XmlFile ?: return + val pomFile = PomFile(xmlFile) + + val tag = element.getParentOfType(false) ?: return + val domElement = DomManager.getDomManager(project).getDomElement(tag) as? GenericDomValue<*> ?: return + val dir = domElement.rawText ?: return + + val relevantExecutions = if (domElement.getParentOfType(MavenDomBuild::class.java, false)?.sourceDirectory === domElement) { + pomFile.findKotlinExecutions(PomFile.KotlinGoals.Compile, PomFile.KotlinGoals.Js) + } else if (domElement.getParentOfType(MavenDomBuild::class.java, false)?.testSourceDirectory === domElement) { + pomFile.findKotlinExecutions(PomFile.KotlinGoals.TestCompile, PomFile.KotlinGoals.TestJs) + } else { + emptyList() + } + + if (relevantExecutions.isNotEmpty()) { + relevantExecutions.forEach { execution -> + val existingSourceDirs = pomFile.executionSourceDirs(execution) + pomFile.executionSourceDirs(execution, (existingSourceDirs + dir).distinct(), true) + } + + domElement.undefine() + } + } +} + +class MavenPluginSourcesMoveToBuild : PsiElementBaseIntentionAction() { + override fun getFamilyName() = "Move to build>sourceDirectory tag" + override fun getText() = familyName + + override fun isAvailable(project: Project, editor: Editor, element: PsiElement): Boolean { + return tryInvoke(project, element) + } + + override fun startInWriteAction() = true + + override fun invoke(project: Project, editor: Editor, element: PsiElement) { + tryInvoke(project, element) { pom, dir, execution, build -> + pom.executionSourceDirs(execution, listOf(dir)) + } + } + + private fun tryInvoke(project: Project, element: PsiElement, block: (pom: PomFile, dir: String, execution: MavenDomPluginExecution, build: MavenDomBuild) -> Unit = { p, d, e, b -> }): Boolean { + val file = element.containingFile + + if (file == null || !MavenDomUtil.isMavenFile(file) || (element !is XmlElement && element.parent !is XmlElement)) { + return false + } + + val tag = element.getParentOfType(false) ?: return false + val domElement = DomManager.getDomManager(project).getDomElement(tag) ?: return false + + val execution = domElement.getParentOfType(MavenDomPluginExecution::class.java, false) ?: return false + tag.parentsWithSelf + .takeWhile { it != execution.xmlElement } + .filterIsInstance() + .firstOrNull { it.localName == "sourceDirs" } ?: return false + + val pom = PomFile(element.containingFile as XmlFile) + val sourceDirsToMove = pom.executionSourceDirs(execution) + + if (sourceDirsToMove.size != 1) { + return false + } + + val build = execution.getParentOfType(MavenDomBuild::class.java, false) ?: return false + var couldMove = 0 + if (shouldMoveCompileSourceRoot(execution)) { + if (!build.sourceDirectory.exists() || build.sourceDirectory.stringValue == sourceDirsToMove.single()) { + couldMove ++ + } + } + if (shouldMoveTestSourceRoot(execution)) { + if (!build.testSourceDirectory.exists() || build.testSourceDirectory.stringValue == sourceDirsToMove.single()) { + couldMove ++ + } + } + + if (couldMove == 1) { + block(pom, sourceDirsToMove.single(), execution, build) + return true + } else { + return false + } + } + + private fun shouldMoveCompileSourceRoot(execution: MavenDomPluginExecution) = + execution.goals.goals.any { it.stringValue == PomFile.KotlinGoals.Compile || it.stringValue == PomFile.KotlinGoals.Js } + + private fun shouldMoveTestSourceRoot(execution: MavenDomPluginExecution) = + execution.goals.goals.any { it.stringValue == PomFile.KotlinGoals.TestCompile || it.stringValue == PomFile.KotlinGoals.TestJs } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/configuration/PomFile.kt b/idea/src/org/jetbrains/kotlin/idea/configuration/PomFile.kt index 739dfba7ca7..b5afef263bc 100644 --- a/idea/src/org/jetbrains/kotlin/idea/configuration/PomFile.kt +++ b/idea/src/org/jetbrains/kotlin/idea/configuration/PomFile.kt @@ -16,6 +16,10 @@ package org.jetbrains.kotlin.idea.configuration +import com.intellij.openapi.module.Module +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.roots.SourceFolder +import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiWhiteSpace @@ -29,14 +33,18 @@ import org.jetbrains.idea.maven.dom.MavenDomUtil import org.jetbrains.idea.maven.dom.model.* import org.jetbrains.idea.maven.model.MavenId import org.jetbrains.idea.maven.utils.MavenArtifactScope +import org.jetbrains.jps.model.java.JavaSourceRootType +import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType import java.util.* class PomFile(val xmlFile: XmlFile) { private val domModel = MavenDomUtil.getMavenDomProjectModel(xmlFile.project, xmlFile.virtualFile) ?: throw IllegalStateException("No DOM model found for pom ${xmlFile.name}") private val nodesByName = HashMap() - private var projectElement: XmlTag? = null + private val projectElement: XmlTag init { + var projectElement: XmlTag? = null + xmlFile.document?.accept(object : PsiElementVisitor() { override fun visitElement(element: PsiElement) { super.visitElement(element) @@ -53,32 +61,35 @@ class PomFile(val xmlFile: XmlFile) { } } }) + + require(projectElement != null) { "pom file should have project element" } + this.projectElement = projectElement!! } fun addProperty(name: String, value: String) { - projectElement?.let { project -> - val properties = ensureElement(project, "properties") - val existing = properties.children.filterIsInstance().filter { it.localName == name } + val properties = ensureElement(projectElement, "properties") + val existing = properties.children.filterIsInstance().filter { it.localName == name } - if (existing.isNotEmpty()) { - for (tag in existing) { - val textNode = tag.children.filterIsInstance().firstOrNull() - if (textNode != null) { - textNode.value = value - } - else { - tag.replace(project.createChildTag(name, project.namespace, value, false)) - } + if (existing.isNotEmpty()) { + for (tag in existing) { + val textNode = tag.children.filterIsInstance().firstOrNull() + if (textNode != null) { + textNode.value = value + } + else { + tag.replace(projectElement.createChildTag(name, value)) } } - else { - properties.add(project.createChildTag(name, project.namespace, value, false)) - } + } + else { + properties.add(projectElement.createChildTag(name, value)) } } fun addDependency(artifact: MavenId, scope: MavenArtifactScope? = null, classifier: String? = null, optional: Boolean = false, systemPath: String? = null): MavenDomDependency { require(systemPath == null || scope == MavenArtifactScope.SYSTEM) { "systemPath is only applicable for system scope dependency" } + require(artifact.groupId != null) { "groupId shouldn't be null" } + require(artifact.artifactId != null) { "artifactId shouldn't be null" } ensureDependencies() val versionless = artifact.withNoVersion() @@ -102,6 +113,8 @@ class PomFile(val xmlFile: XmlFile) { return dependency } + fun addKotlinPlugin(version: String?) = addPlugin(MavenId(KotlinMavenConfigurator.GROUP_ID, KotlinMavenConfigurator.MAVEN_PLUGIN_ID, version)) + fun addPlugin(artifact: MavenId): MavenDomPlugin { ensureBuild() @@ -109,12 +122,21 @@ class PomFile(val xmlFile: XmlFile) { val plugin = domModel.build.plugins.plugins.firstOrNull { it.matches(groupArtifact) } ?: domModel.build.plugins.addPlugin() plugin.groupId.stringValue = artifact.groupId plugin.artifactId.stringValue = artifact.artifactId - plugin.version.stringValue = artifact.version + if (artifact.version != null) { + plugin.version.stringValue = artifact.version + } plugin.ensureTagExists() return plugin } + fun findKotlinPlugins() = domModel.build.plugins.plugins.filter { it.isKotlinMavenPlugin() } + fun findKotlinExecutions(vararg goals: String) = findKotlinExecutions().filter { it.goals.goals.any { it.rawText in goals } } + fun findKotlinExecutions() = findKotlinPlugins().flatMap { it.executions.executions } + + fun findExecutions(plugin: MavenDomPlugin) = plugin.executions.executions + fun findExecutions(plugin: MavenDomPlugin, vararg goals: String) = findExecutions(plugin).filter { it.goals.goals.any { it.rawText in goals } } + fun addExecution(plugin: MavenDomPlugin, executionId: String, phase: String, goals: List): MavenDomPluginExecution { require(goals.isNotEmpty()) { "Execution $executionId requires at least one goal but empty list has been provided" } require(executionId.isNotEmpty()) { "executionId shouldn't be empty" } @@ -127,17 +149,29 @@ class PomFile(val xmlFile: XmlFile) { val existingGoals = execution.goals.goals.mapNotNull { it.rawText } for (goal in goals.filter { it !in existingGoals }) { - val goalTag = execution.goals.xmlTag.createChildTag("goal", plugin.xmlElementNamespace, goal, false) + val goalTag = execution.goals.xmlTag.createChildTag("goal", goal) execution.goals.xmlTag.add(goalTag) } return execution } - fun executionSourceDirs(execution: MavenDomPluginExecution, sourceDirs: List) { + fun addKotlinExecution(module: Module, plugin: MavenDomPlugin, executionId: String, phase: String, isTest: Boolean, goals: List) { + val execution = addExecution(plugin, executionId, phase, goals) + + val sourceDirs = ModuleRootManager.getInstance(module) + .contentEntries + .flatMap { it.sourceFolders.filter { it.isRelatedSourceRoot(isTest) } } + .mapNotNull { it.file } + .mapNotNull { VfsUtilCore.getRelativePath(it, xmlFile.virtualFile.parent, '/') } + + executionSourceDirs(execution, sourceDirs) + } + + fun executionSourceDirs(execution: MavenDomPluginExecution, sourceDirs: List, forceSingleSource: Boolean = false) { ensureBuild() - val isTest = execution.goals.goals.any { it.stringValue?.let { "test" in it } ?: false } + val isTest = execution.goals.goals.any { it.stringValue == KotlinGoals.TestCompile || it.stringValue == KotlinGoals.TestJs} val defaultDir = if (isTest) "test" else "main" val singleDirectoryElement = if (isTest) { domModel.build.testSourceDirectory @@ -150,7 +184,7 @@ class PomFile(val xmlFile: XmlFile) { execution.configuration.xmlTag?.findSubTags("sourceDirs")?.forEach { it.deleteCascade() } singleDirectoryElement.undefine() } - else if (sourceDirs.size == 1) { + else if (sourceDirs.size == 1 && !forceSingleSource) { singleDirectoryElement.stringValue = sourceDirs.single() execution.configuration.xmlTag?.findSubTags("sourceDirs")?.forEach { it.deleteCascade() } } @@ -164,6 +198,14 @@ class PomFile(val xmlFile: XmlFile) { } } + fun executionSourceDirs(execution: MavenDomPluginExecution): List { + return execution.configuration.xmlTag + .getChildrenOfType().firstOrNull { it.localName == "sourceDirs" } + ?.getChildrenOfType() + ?.map { it.getChildrenOfType().joinToString("") { it.text } } + ?: emptyList() + } + fun executionConfiguration(execution: MavenDomPluginExecution, name: String): XmlTag { val configurationTag = execution.configuration.ensureTagExists()!! @@ -172,7 +214,7 @@ class PomFile(val xmlFile: XmlFile) { return existingTag } - val newTag = configurationTag.createChildTag(name, configurationTag.namespace, null, false)!! + val newTag = configurationTag.createChildTag(name) return configurationTag.add(newTag) as XmlTag } @@ -215,12 +257,22 @@ class PomFile(val xmlFile: XmlFile) { fun hasPlugin(artifact: MavenId) = domModel.build.plugins.plugins.any { it.matches(artifact) } fun hasDependency(artifact: MavenId, scope: MavenArtifactScope? = null) = - domModel.dependencies.dependencies.any { it.matches(artifact) && (it.scope.stringValue == scope?.name || scope == null && it.scope.stringValue == "compile") } + domModel.dependencies.dependencies.any { it.matches(artifact, scope) } - fun ensureBuild(): XmlTag = ensureElement(projectElement!!, "build") - fun ensureDependencies(): XmlTag = ensureElement(projectElement!!, "dependencies") - fun ensurePluginRepositories(): XmlTag = ensureElement(projectElement!!, "pluginRepositories") - fun ensureRepositories(): XmlTag = ensureElement(projectElement!!, "repositories") + fun findDependencies(artifact: MavenId, scope: MavenArtifactScope? = null) = + domModel.dependencies.dependencies.filter { it.matches(artifact, scope) } + + fun ensureBuild(): XmlTag = ensureElement(projectElement, "build") + + fun ensureDependencies(): XmlTag = ensureElement(projectElement, "dependencies") + fun ensurePluginRepositories(): XmlTag = ensureElement(projectElement, "pluginRepositories") + fun ensureRepositories(): XmlTag = ensureElement(projectElement, "repositories") + + private fun MavenDomPlugin.isKotlinMavenPlugin() = groupId.stringValue == KotlinMavenConfigurator.GROUP_ID + && artifactId.stringValue == KotlinMavenConfigurator.MAVEN_PLUGIN_ID + + private fun MavenDomDependency.matches(artifact: MavenId, scope: MavenArtifactScope?) = + this.matches(artifact) && (this.scope.stringValue == scope?.name?.toLowerCase() || scope == null && this.scope.stringValue == "compile") private fun MavenDomArtifactCoordinates.matches(artifact: MavenId) = (artifact.groupId == null || groupId.stringValue == artifact.groupId) @@ -230,8 +282,9 @@ class PomFile(val xmlFile: XmlFile) { private fun MavenId.withNoVersion() = MavenId(groupId, artifactId, null) private fun MavenDomElement.createChildTag(name: String, value: String? = null) = xmlTag.createChildTag(name, value) - private fun XmlTag.createChildTag(name: String, value: String? = null) = createChildTag(name, namespace, value, false) + private fun XmlTag.createChildTag(name: String, value: String? = null) = createChildTag(name, namespace, value, false)!! + tailrec private fun XmlTag.deleteCascade() { val oldParent = this.parentTag delete() @@ -296,7 +349,61 @@ class PomFile(val xmlFile: XmlFile) { private fun GenericDomValue.isEmpty() = !exists() || stringValue.isNullOrEmpty() + private fun SourceFolder.isRelatedSourceRoot(isTest: Boolean): Boolean { + val relevantRootType = when { + isTest -> JavaSourceRootType.TEST_SOURCE + else -> JavaSourceRootType.SOURCE + } + + return rootType === relevantRootType + } + + @Suppress("Unused") + object DefaultPhases { + val Validate = "validate" + val Initialize = "initialize" + val GenerateSources = "generate-sources" + val ProcessSources = "process-sources" + val GenerateResources = "generate-resources" + val ProcessResources = "process-resources" + val Compile = "compile" + val ProcessClasses = "process-classes" + val GenerateTestSources = "generate-test-sources" + val ProcessTestSources = "process-test-sources" + val GenerateTestResources = "generate-test-resources" + val ProcessTestResources = "process-test-resources" + val TestCompile = "test-compile" + val ProcessTestClasses = "process-test-classes" + val Test = "test" + val PreparePackage = "prepare-package" + val Package = "package" + val PreIntegrationTest = "pre-integration-test" + val IntegrationTest = "integration-test" + val PostIntegrationTest = "post-integration-test" + val Verify = "verify" + val Install = "install" + val Deploy = "deploy" + } + + object KotlinGoals { + val Compile = "compile" + val TestCompile = "test-compile" + val Js = "js" + val TestJs = "test-js" + } + companion object { + fun getPhase(hasJavaFiles: Boolean, isTest: Boolean) = when { + hasJavaFiles -> when { + isTest -> DefaultPhases.ProcessTestSources + else -> DefaultPhases.ProcessSources + } + else -> when { + isTest -> DefaultPhases.TestCompile + else -> DefaultPhases.Compile + } + } + // from maven code convention: https://maven.apache.org/developers/conventions/code.html val recommendedElementsOrder = """ diff --git a/idea/testData/maven-inspections/bothCompileAndTestCompileInTheSameExecution.xml b/idea/testData/maven-inspections/bothCompileAndTestCompileInTheSameExecution.xml new file mode 100644 index 00000000000..fbfbac3416e --- /dev/null +++ b/idea/testData/maven-inspections/bothCompileAndTestCompileInTheSameExecution.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + org.jetbrains.kotlin.test + configure-maven-test + 1.0-SNAPSHOT + + + 1.0.1 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + test-compile + + + + + + + + + \ No newline at end of file diff --git a/idea/testData/maven-inspections/dependencyWithNoExecution.fixed.1.xml b/idea/testData/maven-inspections/dependencyWithNoExecution.fixed.1.xml new file mode 100644 index 00000000000..c98a0ae9072 --- /dev/null +++ b/idea/testData/maven-inspections/dependencyWithNoExecution.fixed.1.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + org.jetbrains.kotlin.test + configure-maven-test + 1.0-SNAPSHOT + + + 1.0.1 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-js-library + ${kotlin.version} + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + js + + + + + + + + + + + + diff --git a/idea/testData/maven-inspections/dependencyWithNoExecution.fixed.2.xml b/idea/testData/maven-inspections/dependencyWithNoExecution.fixed.2.xml new file mode 100644 index 00000000000..5821d022c5f --- /dev/null +++ b/idea/testData/maven-inspections/dependencyWithNoExecution.fixed.2.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + org.jetbrains.kotlin.test + configure-maven-test + 1.0-SNAPSHOT + + + 1.0.1 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-js-library + ${kotlin.version} + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + + + + + + + + + diff --git a/idea/testData/maven-inspections/dependencyWithNoExecution.xml b/idea/testData/maven-inspections/dependencyWithNoExecution.xml new file mode 100644 index 00000000000..d0c18d857a1 --- /dev/null +++ b/idea/testData/maven-inspections/dependencyWithNoExecution.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + org.jetbrains.kotlin.test + configure-maven-test + 1.0-SNAPSHOT + + + 1.0.1 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-js-library + ${kotlin.version} + + + + + + diff --git a/idea/testData/maven-inspections/missingDependencies.fixed.1.xml b/idea/testData/maven-inspections/missingDependencies.fixed.1.xml new file mode 100644 index 00000000000..ce01232550d --- /dev/null +++ b/idea/testData/maven-inspections/missingDependencies.fixed.1.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + org.jetbrains.kotlin.test + configure-maven-test + 1.0-SNAPSHOT + + + 1.0.1 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + js + + + + + + + + + + diff --git a/idea/testData/maven-inspections/missingDependencies.fixed.2.xml b/idea/testData/maven-inspections/missingDependencies.fixed.2.xml new file mode 100644 index 00000000000..209e312e947 --- /dev/null +++ b/idea/testData/maven-inspections/missingDependencies.fixed.2.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + org.jetbrains.kotlin.test + configure-maven-test + 1.0-SNAPSHOT + + + 1.0.1 + + + + + org.jetbrains.kotlin + kotlin-js-library + ${kotlin.version} + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + js + + + + + + + + + + diff --git a/idea/testData/maven-inspections/missingDependencies.xml b/idea/testData/maven-inspections/missingDependencies.xml new file mode 100644 index 00000000000..b01241c7a13 --- /dev/null +++ b/idea/testData/maven-inspections/missingDependencies.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + org.jetbrains.kotlin.test + configure-maven-test + 1.0-SNAPSHOT + + + 1.0.1 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + js + + + + + + + + + + diff --git a/idea/testData/maven-inspections/noExecutions.fixed.1.xml b/idea/testData/maven-inspections/noExecutions.fixed.1.xml new file mode 100644 index 00000000000..2917e60e5bd --- /dev/null +++ b/idea/testData/maven-inspections/noExecutions.fixed.1.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + org.jetbrains.kotlin.test + configure-maven-test + 1.0-SNAPSHOT + + + 1.0.1 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + + + + + + \ No newline at end of file diff --git a/idea/testData/maven-inspections/noExecutions.fixed.2.xml b/idea/testData/maven-inspections/noExecutions.fixed.2.xml new file mode 100644 index 00000000000..4aebf68a618 --- /dev/null +++ b/idea/testData/maven-inspections/noExecutions.fixed.2.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + org.jetbrains.kotlin.test + configure-maven-test + 1.0-SNAPSHOT + + + 1.0.1 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + js + compile + + js + + + + + + + + + \ No newline at end of file diff --git a/idea/testData/maven-inspections/noExecutions.xml b/idea/testData/maven-inspections/noExecutions.xml new file mode 100644 index 00000000000..275801aff34 --- /dev/null +++ b/idea/testData/maven-inspections/noExecutions.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + org.jetbrains.kotlin.test + configure-maven-test + 1.0-SNAPSHOT + + + 1.0.1 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + + + + \ No newline at end of file diff --git a/idea/testData/maven-inspections/wrongJsExecution.xml b/idea/testData/maven-inspections/wrongJsExecution.xml new file mode 100644 index 00000000000..2936951187c --- /dev/null +++ b/idea/testData/maven-inspections/wrongJsExecution.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + org.jetbrains.kotlin.test + configure-maven-test + 1.0-SNAPSHOT + + + 1.0.1 + + + + + org.jetbrains.kotlin + kotlin-js-library + ${kotlin.version} + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + js + + + + + + + + + + \ No newline at end of file diff --git a/idea/testData/maven-inspections/wrongPhaseExecution.fixed.1.xml b/idea/testData/maven-inspections/wrongPhaseExecution.fixed.1.xml new file mode 100644 index 00000000000..e0f030f8876 --- /dev/null +++ b/idea/testData/maven-inspections/wrongPhaseExecution.fixed.1.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + org.jetbrains.kotlin.test + configure-maven-test + 1.0-SNAPSHOT + + + 1.0.1 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + process-sources + + compile + + + + + + + + + + \ No newline at end of file diff --git a/idea/testData/maven-inspections/wrongPhaseExecution.xml b/idea/testData/maven-inspections/wrongPhaseExecution.xml new file mode 100644 index 00000000000..b2a8679c973 --- /dev/null +++ b/idea/testData/maven-inspections/wrongPhaseExecution.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + org.jetbrains.kotlin.test + configure-maven-test + 1.0-SNAPSHOT + + + 1.0.1 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + + + + + + + \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/maven/AbstractKotlinMavenInspectionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/maven/AbstractKotlinMavenInspectionTest.kt new file mode 100644 index 00000000000..b0833cc240c --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/maven/AbstractKotlinMavenInspectionTest.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2010-2016 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.maven + +import com.intellij.analysis.AnalysisScope +import com.intellij.codeInspection.* +import com.intellij.codeInspection.ex.InspectionManagerEx +import com.intellij.codeInspection.ex.LocalInspectionToolWrapper +import com.intellij.ide.highlighter.JavaFileType +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.Result +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.command.CommandProcessor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiManager +import com.intellij.psi.search.FileTypeIndex +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.testFramework.InspectionTestUtil +import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl +import com.intellij.util.indexing.FileBasedIndex +import org.jetbrains.jps.model.java.JavaSourceRootType +import org.jetbrains.kotlin.idea.configuration.KotlinMavenPluginPhaseInspection +import org.jetbrains.kotlin.idea.refactoring.toPsiDirectory +import org.jetbrains.kotlin.idea.util.projectStructure.allModules +import java.io.File + +abstract class AbstractKotlinMavenInspectionTest : MavenImportingTestCase() { + + override fun setUp() { + super.setUp() + repositoryPath = File(myDir, "repo").path + createStdProjectFolders() + } + + fun doTest(fileName: String) { + val pomFile = File(fileName) + val pomText = pomFile.readText() + + createPomFile(fileName) + importProject() + myProject.allModules().forEach { + setupJdkForModule(it.name) + } + + if (pomText.contains("".toRegex(RegexOption.MULTILINE))) { + mkJavaFile() + } + + val matcher = "".toRegex() + val expected = pomText.lines().mapNotNull { matcher.find(it) }.map { SimplifiedProblemDescription(it.groups[2]!!.value.trim(), it.groups[1]!!.value.trim()) } + val actual = runInspection().sortedBy { it.first.text } + + assertEquals(expected.sortedBy { it.text }, actual.map { it.first }) + + val suggestedFixes = actual.flatMap { p -> p.second.fixes?.sortedBy { it.familyName }?.map { p.second to it } ?: emptyList() } + + val filenamePrefix = pomFile.nameWithoutExtension + ".fixed." + val fixFiles = pomFile.parentFile.listFiles { file, name -> name.startsWith(filenamePrefix) && name.endsWith(".xml") }.sortedBy { it.name } + + if (fixFiles.size > suggestedFixes.size) { + fail("Not all fixes were suggested by the inspection: expected count: ${fixFiles.size}, actual fixes count: ${suggestedFixes.size}") + } + if (fixFiles.size < suggestedFixes.size) { + fail("Not all fixes covered by *.fixed.N.xml files") + } + + val documentManager = PsiDocumentManager.getInstance(myProject) + val document = documentManager.getDocument(PsiManager.getInstance(myProject).findFile(myProjectPom)!!)!! + val originalText = document.text + + fixFiles.forEachIndexed { index, file -> + val (problem, quickfix) = suggestedFixes[index] + + quickfix.applyFix(problem) + + assertEquals(file.readText().trim(), document.text.trim()) + + ApplicationManager.getApplication().runWriteAction { + document.setText(originalText) + documentManager.commitDocument(document) + } + } + } + + private fun createPomFile(fileName: String) { + myProjectPom = myProjectRoot.findChild("pom.xml") + if (myProjectPom == null) { + myProjectPom = object : WriteAction() { + override fun run(result: Result) { + val res = myProjectRoot.createChildData(null, "pom.xml") + result.setResult(res) + } + }.execute().resultObject + } + myAllPoms.add(myProjectPom!!) + + ApplicationManager.getApplication().runWriteAction { + myProjectPom!!.setBinaryContent(File(fileName).readBytes()) + } + } + + private fun QuickFix.applyFix(desc: ProblemDescriptorBase) { + CommandProcessor.getInstance().executeCommand(myProject, { + ApplicationManager.getApplication().runWriteAction { + applyFix(myProject, desc) + + val manager = PsiDocumentManager.getInstance(myProject) + val document = manager.getDocument(PsiManager.getInstance(myProject).findFile(myProjectPom)!!)!! + manager.doPostponedOperationsAndUnblockDocument(document) + manager.commitDocument(document) + FileDocumentManager.getInstance().saveDocument(document) + + } + + println(myProjectPom.contentsToByteArray().toString(Charsets.UTF_8)) + }, "quick-fix-$name", "Kotlin") + } + + private fun mkJavaFile() { + val sourceFolder = getContentRoots(myProject.allModules().single().name).single().getSourceFolders(JavaSourceRootType.SOURCE).single() + ApplicationManager.getApplication().runWriteAction { + val javaFile = sourceFolder.file?.toPsiDirectory(myProject)?.createFile("Test.java") ?: throw IllegalStateException() + javaFile.virtualFile.setBinaryContent("class Test {}\n".toByteArray()) + + FileBasedIndex.getInstance().ensureUpToDate(FileTypeIndex.NAME, myProject, GlobalSearchScope.projectScope(myProject)) + myProject.allModules().forEach { module -> + FileBasedIndex.getInstance().ensureUpToDate(FileTypeIndex.NAME, myProject, module.moduleScope) + } + } + + assertTrue(FileTypeIndex.containsFileOfType(JavaFileType.INSTANCE, myProject.allModules().single().moduleScope)) + } + + private inline fun runInspection(): List> { + val inspectionClass = T::class.java + + val toolWrapper = LocalInspectionToolWrapper(inspectionClass.newInstance() as LocalInspectionTool) + + val scope = AnalysisScope(myProject) + val inspectionManager = (InspectionManager.getInstance(myProject) as InspectionManagerEx) + val globalContext = CodeInsightTestFixtureImpl.createGlobalContextForTool(scope, myProject, inspectionManager, toolWrapper) + + InspectionTestUtil.runTool(toolWrapper, scope, globalContext) + val presentation = globalContext.getPresentation(toolWrapper) + + return presentation.problemElements.filter { it.key.name == "pom.xml" } + .values + .flatMap { it.toList() } + .mapNotNull { it as? ProblemDescriptorBase } + .map { SimplifiedProblemDescription(it.descriptionTemplate, it.psiElement.text.replace("\\s+".toRegex(), "")) to it } + } + + private data class SimplifiedProblemDescription(val text: String, val elementText: String) +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/maven/KotlinMavenInspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/maven/KotlinMavenInspectionTestGenerated.java new file mode 100644 index 00000000000..f99db69a8a6 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/maven/KotlinMavenInspectionTestGenerated.java @@ -0,0 +1,73 @@ +/* + * Copyright 2010-2016 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.maven; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.KotlinTestUtils; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("idea/testData/maven-inspections") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class KotlinMavenInspectionTestGenerated extends AbstractKotlinMavenInspectionTest { + public void testAllFilesPresentInMaven_inspections() throws Exception { + KotlinTestUtils.assertAllTestsPresentInSingleGeneratedClass(this.getClass(), new File("idea/testData/maven-inspections"), Pattern.compile("^([\\w\\-]+).xml$")); + } + + @TestMetadata("bothCompileAndTestCompileInTheSameExecution.xml") + public void testBothCompileAndTestCompileInTheSameExecution() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/maven-inspections/bothCompileAndTestCompileInTheSameExecution.xml"); + doTest(fileName); + } + + @TestMetadata("dependencyWithNoExecution.xml") + public void testDependencyWithNoExecution() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/maven-inspections/dependencyWithNoExecution.xml"); + doTest(fileName); + } + + @TestMetadata("missingDependencies.xml") + public void testMissingDependencies() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/maven-inspections/missingDependencies.xml"); + doTest(fileName); + } + + @TestMetadata("noExecutions.xml") + public void testNoExecutions() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/maven-inspections/noExecutions.xml"); + doTest(fileName); + } + + @TestMetadata("wrongJsExecution.xml") + public void testWrongJsExecution() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/maven-inspections/wrongJsExecution.xml"); + doTest(fileName); + } + + @TestMetadata("wrongPhaseExecution.xml") + public void testWrongPhaseExecution() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/maven-inspections/wrongPhaseExecution.xml"); + doTest(fileName); + } +}