diff --git a/ChangeLog.md b/ChangeLog.md index da26b1834bd..d4cc0e4fe37 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -90,6 +90,7 @@ These artifacts include extensions for the types available in the latter JDKs, s ###### New features - Show versions in inspection about different version of Kotlin plugin in Maven and IDE plugin +- [`KT-12730`](https://youtrack.jetbrains.com/issue/KT-12730) Warn about using different versions of Kotlin Gradle plugin and bundled compiler. ###### Issues fixed diff --git a/idea/resources/inspectionDescriptions/DifferentKotlinGradleVersion.html b/idea/resources/inspectionDescriptions/DifferentKotlinGradleVersion.html new file mode 100644 index 00000000000..ec6de22c0bb --- /dev/null +++ b/idea/resources/inspectionDescriptions/DifferentKotlinGradleVersion.html @@ -0,0 +1,6 @@ + + +This inspection reports different IDE and Gradle plugin versions are used. +This can cause inconsistencies between IDE and Gradle build in error reporting or code behaviour. + + \ No newline at end of file diff --git a/idea/src/META-INF/gradle.xml b/idea/src/META-INF/gradle.xml index 36ad1c822ba..075f561f1e6 100644 --- a/idea/src/META-INF/gradle.xml +++ b/idea/src/META-INF/gradle.xml @@ -1,9 +1,19 @@ - - - - - - - + + + + + + + + + + diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/gradle/DifferentKotlinGradleVersionInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/gradle/DifferentKotlinGradleVersionInspection.kt new file mode 100644 index 00000000000..40b10f6993c --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/gradle/DifferentKotlinGradleVersionInspection.kt @@ -0,0 +1,161 @@ +/* + * 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.inspections.gradle + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.openapi.util.io.FileUtilRt +import org.jetbrains.annotations.TestOnly +import org.jetbrains.kotlin.idea.KotlinPluginUtil +import org.jetbrains.kotlin.idea.versions.bundledRuntimeVersion +import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType +import org.jetbrains.plugins.gradle.codeInspection.GradleBaseInspection +import org.jetbrains.plugins.gradle.util.GradleConstants +import org.jetbrains.plugins.groovy.codeInspection.BaseInspection +import org.jetbrains.plugins.groovy.codeInspection.BaseInspectionVisitor +import org.jetbrains.plugins.groovy.lang.psi.GroovyFileBase +import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariableDeclaration +import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrAssignmentExpression +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrCommandArgumentList +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrString +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrCallExpression +import java.util.* + +class DifferentKotlinGradleVersionInspection : GradleBaseInspection() { + var testVersionMessage: String? = null + @TestOnly set + + override fun buildVisitor(): BaseInspectionVisitor = MyVisitor() + + override fun getGroupDisplayName() = BaseInspection.PROBABLE_BUGS + + override fun buildErrorString(vararg args: Any): String { + return "Kotlin version that is used for building with Gradle (${args[0]}) differs from the one bundled into the IDE plugin (${args[1]})" + } + + private inner class MyVisitor : BaseInspectionVisitor() { + private val idePluginVersion by lazy { bundledRuntimeVersion() } + + override fun visitFile(file: GroovyFileBase?) { + if (file == null || !FileUtilRt.extensionEquals(file.name, GradleConstants.EXTENSION)) return + + val fileIndex = ProjectRootManager.getInstance(file.project).fileIndex + + if (!ApplicationManager.getApplication().isUnitTestMode) { + val module = fileIndex.getModuleForFile(file.virtualFile) ?: return + if (!KotlinPluginUtil.isGradleModule(module)) return + } + + if (fileIndex.isExcluded(file.virtualFile)) return + + super.visitFile(file) + } + + override fun visitClosure(closure: GrClosableBlock) { + super.visitClosure(closure) + + val dependenciesCall = closure.getStrictParentOfType() ?: return + if (dependenciesCall.invokedExpression.text != "dependencies") return + + val buildScriptCall = dependenciesCall.getStrictParentOfType() ?: return + if (buildScriptCall.invokedExpression.text != "buildscript") return + + val kotlinPluginStatement = findClassPathStatements(closure).filter { + it.text.contains("org.jetbrains.kotlin:kotlin-gradle-plugin:") + }.firstOrNull() ?: return + + val kotlinPluginVersion = + getKotlinPluginVersion(kotlinPluginStatement) ?: + return + + if (kotlinPluginVersion != idePluginVersion) { + registerError(kotlinPluginStatement, kotlinPluginVersion, testVersionMessage ?: idePluginVersion) + } + } + + private fun getKotlinPluginVersion(classpathStatement: GrCallExpression): String? { + val argument = classpathStatement.getChildrenOfType().firstOrNull() ?: return null + val grLiteral = argument.children.firstOrNull()?.let { it as? GrLiteral } ?: return null + + if (grLiteral is GrString && grLiteral.injections.size == 1) { + val versionInjection = grLiteral.injections.first() ?: return null + val expression = versionInjection.expression as? GrReferenceExpression ?: + versionInjection.closableBlock?.getChildrenOfType()?.singleOrNull() ?: + return null + + return resolveVariableInBuildScript(classpathStatement, expression.text) + } + + val literalValue = grLiteral.value ?: return null + val versionText = literalValue.toString().substringAfterLast(':') + if (versionText.isEmpty()) return null + + return versionText + } + + private fun resolveVariableInBuildScript(classpathStatement: GrCallExpression, name: String): String? { + val dependenciesClosure = classpathStatement.getStrictParentOfType() ?: return null + val buildScriptClosure = dependenciesClosure.getStrictParentOfType() ?: return null + + for (child in buildScriptClosure.children) { + when (child) { + is GrAssignmentExpression -> { + if (child.lValue.text == "ext.$name") { + val assignValue = child.rValue + if (assignValue is GrLiteral) { + return assignValue.value.toString() + } + } + } + is GrVariableDeclaration -> { + for (variable in child.variables) { + if (variable.name == name) { + val assignValue = variable.initializerGroovy + if (assignValue is GrLiteral) { + return assignValue.value.toString() + } + } + } + } + } + } + + return null + } + + private fun findClassPathStatements(closure: GrClosableBlock): List { + val applicationStatements = closure.getChildrenOfType() + + val classPathStatements = ArrayList() + + for (statement in applicationStatements) { + val startExpression = statement.getChildrenOfType().firstOrNull() ?: continue + if ("classpath" == startExpression.text) { + classPathStatements.add(statement) + } + } + + return classPathStatements + } + } +} + diff --git a/idea/testData/inspections/gradleWrongPluginVersion/fullHelloWorldConfiguration.gradle b/idea/testData/inspections/gradleWrongPluginVersion/fullHelloWorldConfiguration.gradle new file mode 100644 index 00000000000..aa0388f2bb1 --- /dev/null +++ b/idea/testData/inspections/gradleWrongPluginVersion/fullHelloWorldConfiguration.gradle @@ -0,0 +1,30 @@ +group 'OnlyGradleTest' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '0.0.1' + + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + + +apply plugin: 'groovy' +apply plugin: 'java' +apply plugin: 'kotlin' + +sourceCompatibility = 1.5 + +repositories { + mavenCentral() +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + compile 'org.codehaus.groovy:groovy-all:2.3.+' + testCompile group: 'junit', name: 'junit', version: '4.11' +} diff --git a/idea/testData/inspections/gradleWrongPluginVersion/inspectionData/expected.xml b/idea/testData/inspections/gradleWrongPluginVersion/inspectionData/expected.xml new file mode 100644 index 00000000000..31a12cf600d --- /dev/null +++ b/idea/testData/inspections/gradleWrongPluginVersion/inspectionData/expected.xml @@ -0,0 +1,55 @@ + + + wrongPluginVersionWithNumber.gradle + 6 + light_idea_test_case + + Kotlin Gradle and IDE plugins versions are different + Kotlin version that is used for building with Gradle (0.0.1) differs from the one bundled into the IDE plugin ($PLUGIN_VERSION) + + + + wrongPluginVersionWithStandardKotlinVariable.gradle + 9 + light_idea_test_case + + Kotlin Gradle and IDE plugins versions are different + Kotlin version that is used for building with Gradle (0.0.1) differs from the one bundled into the IDE plugin ($PLUGIN_VERSION) + + + + fullHelloWorldConfiguration.gradle + 11 + light_idea_test_case + + Kotlin Gradle and IDE plugins versions are different + Kotlin version that is used for building with Gradle (0.0.1) differs from the one bundled into the IDE plugin ($PLUGIN_VERSION) + + + + wrongPluginVersionWithSomeVariable.gradle + 9 + light_idea_test_case + + Kotlin Gradle and IDE plugins versions are different + Kotlin version that is used for building with Gradle (0.0.1) differs from the one bundled into the IDE plugin ($PLUGIN_VERSION) + + + + wrongPluginVersionWithSomeVariableInDef.gradle + 9 + light_idea_test_case + + Kotlin Gradle and IDE plugins versions are different + Kotlin version that is used for building with Gradle (0.0.1) differs from the one bundled into the IDE plugin ($PLUGIN_VERSION) + + + + wrongPluginVersionWithVariableAndFullInterpolation.gradle + 9 + light_idea_test_case + + Kotlin Gradle and IDE plugins versions are different + Kotlin version that is used for building with Gradle (0.0.1) differs from the one bundled into the IDE plugin ($PLUGIN_VERSION) + + diff --git a/idea/testData/inspections/gradleWrongPluginVersion/inspectionData/inspections.test b/idea/testData/inspections/gradleWrongPluginVersion/inspectionData/inspections.test new file mode 100644 index 00000000000..d5b92f46934 --- /dev/null +++ b/idea/testData/inspections/gradleWrongPluginVersion/inspectionData/inspections.test @@ -0,0 +1 @@ +// INSPECTION_CLASS: org.jetbrains.kotlin.idea.inspections.gradle.DifferentKotlinGradleVersionInspection \ No newline at end of file diff --git a/idea/testData/inspections/gradleWrongPluginVersion/noWrongPluginVersionOnGoodConfig.gradle b/idea/testData/inspections/gradleWrongPluginVersion/noWrongPluginVersionOnGoodConfig.gradle new file mode 100644 index 00000000000..f4306883162 --- /dev/null +++ b/idea/testData/inspections/gradleWrongPluginVersion/noWrongPluginVersionOnGoodConfig.gradle @@ -0,0 +1,11 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$PLUGIN_VERSION" + } +} + +apply plugin: 'java' +apply plugin: 'kotlin' \ No newline at end of file diff --git a/idea/testData/inspections/gradleWrongPluginVersion/noWrongPluginVersionOnNoKotlinPlugin.gradle b/idea/testData/inspections/gradleWrongPluginVersion/noWrongPluginVersionOnNoKotlinPlugin.gradle new file mode 100644 index 00000000000..7a233c3e056 --- /dev/null +++ b/idea/testData/inspections/gradleWrongPluginVersion/noWrongPluginVersionOnNoKotlinPlugin.gradle @@ -0,0 +1,17 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:something-kotlin-gradle-plugin:0.0.1" + classpath "someone:kotlin-gradle-plugin:0.0.1" + } +} + +apply plugin: 'java' +apply plugin: 'kotlin' + +dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:0.0.1" // Bad position for plugin + classpath "someone:kotlin-gradle-plugin:0.0.1" +} \ No newline at end of file diff --git a/idea/testData/inspections/gradleWrongPluginVersion/noWrongPluginVersionOnNoVersion.gradle b/idea/testData/inspections/gradleWrongPluginVersion/noWrongPluginVersionOnNoVersion.gradle new file mode 100644 index 00000000000..d911bcb3195 --- /dev/null +++ b/idea/testData/inspections/gradleWrongPluginVersion/noWrongPluginVersionOnNoVersion.gradle @@ -0,0 +1,11 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:" + } +} + +apply plugin: 'java' +apply plugin: 'kotlin' \ No newline at end of file diff --git a/idea/testData/inspections/gradleWrongPluginVersion/noWrongPluginVersionWhenSuppressed.gradle b/idea/testData/inspections/gradleWrongPluginVersion/noWrongPluginVersionWhenSuppressed.gradle new file mode 100644 index 00000000000..445ed306778 --- /dev/null +++ b/idea/testData/inspections/gradleWrongPluginVersion/noWrongPluginVersionWhenSuppressed.gradle @@ -0,0 +1,12 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + //noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:0.0.1" + } +} + +apply plugin: 'java' +apply plugin: 'kotlin' \ No newline at end of file diff --git a/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithNumber.gradle b/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithNumber.gradle new file mode 100644 index 00000000000..915df0f836c --- /dev/null +++ b/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithNumber.gradle @@ -0,0 +1,11 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:0.0.1" + } +} + +apply plugin: 'java' +apply plugin: 'kotlin' \ No newline at end of file diff --git a/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithSomeVariable.gradle b/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithSomeVariable.gradle new file mode 100644 index 00000000000..df5cf3dd143 --- /dev/null +++ b/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithSomeVariable.gradle @@ -0,0 +1,14 @@ +buildscript { + ext.some = '0.0.1' + + repositories { + mavenCentral() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$some" + } +} + +apply plugin: 'java' +apply plugin: 'kotlin' \ No newline at end of file diff --git a/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithSomeVariableInDef.gradle b/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithSomeVariableInDef.gradle new file mode 100644 index 00000000000..36039175a06 --- /dev/null +++ b/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithSomeVariableInDef.gradle @@ -0,0 +1,14 @@ +buildscript { + def defVar = '0.0.1' + + repositories { + mavenCentral() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$defVar" + } +} + +apply plugin: 'java' +apply plugin: 'kotlin' \ No newline at end of file diff --git a/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithStandardKotlinVariable.gradle b/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithStandardKotlinVariable.gradle new file mode 100644 index 00000000000..84f77cb6eee --- /dev/null +++ b/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithStandardKotlinVariable.gradle @@ -0,0 +1,14 @@ +buildscript { + ext.kotlin_version = '0.0.1' + + repositories { + mavenCentral() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +apply plugin: 'java' +apply plugin: 'kotlin' \ No newline at end of file diff --git a/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithVariableAndFullInterpolation.gradle b/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithVariableAndFullInterpolation.gradle new file mode 100644 index 00000000000..ab5ad95f4ea --- /dev/null +++ b/idea/testData/inspections/gradleWrongPluginVersion/wrongPluginVersionWithVariableAndFullInterpolation.gradle @@ -0,0 +1,14 @@ +buildscript { + ext.t = '0.0.1' + + repositories { + mavenCentral() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${t}" + } +} + +apply plugin: 'java' +apply plugin: 'kotlin' \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractInspectionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractInspectionTest.kt index 9f111b4f54a..df0739c7f63 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractInspectionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/AbstractInspectionTest.kt @@ -29,8 +29,10 @@ import com.intellij.testFramework.IdeaTestUtil import com.intellij.testFramework.InspectionTestUtil import com.intellij.testFramework.LightProjectDescriptor import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl +import org.jetbrains.kotlin.idea.inspections.gradle.DifferentKotlinGradleVersionInspection import org.jetbrains.kotlin.idea.test.* import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.idea.versions.bundledRuntimeVersion import org.jetbrains.kotlin.test.InTextDirectivesUtils import org.jetbrains.kotlin.test.KotlinTestUtils import java.io.File @@ -63,6 +65,11 @@ abstract class AbstractInspectionTest : KotlinLightCodeInsightFixtureTestCase() val inspectionClass = Class.forName(InTextDirectivesUtils.findStringWithPrefixes(options, "// INSPECTION_CLASS: ")!!) val toolWrapper = LocalInspectionToolWrapper(inspectionClass.newInstance() as LocalInspectionTool) + val tool = toolWrapper.tool + if (tool is DifferentKotlinGradleVersionInspection) { + tool.testVersionMessage = "\$PLUGIN_VERSION" + } + val fixtureClasses = InTextDirectivesUtils.findListWithPrefixes(options, "// FIXTURE_CLASS: ") val inspectionsTestDir = optionsFile.parentFile!! @@ -72,23 +79,27 @@ abstract class AbstractInspectionTest : KotlinLightCodeInsightFixtureTestCase() testDataPath = "${KotlinTestUtils.getHomeDirectory()}/$srcDir" val afterFiles = srcDir.listFiles { it -> it.name == "inspectionData" }?.single()?.listFiles { it -> it.extension == "after" } ?: emptyArray() - val psiFiles = srcDir.walkTopDown().onEnter { it.name != "inspectionData" }.mapNotNull { - file -> - if (file.isDirectory) { - null - } - else if (file.extension != "kt") { - val filePath = file.relativeTo(srcDir).invariantSeparatorsPath - configureByFile(filePath) - } - else { - val text = FileUtil.loadFile(file, true) - val fileText = - if (text.startsWith("package")) - text - else - "package ${file.nameWithoutExtension};$text" - configureByText(file.name, fileText)!! + val psiFiles = srcDir.walkTopDown().onEnter { it.name != "inspectionData" }.mapNotNull { file -> + when { + file.isDirectory -> null + file.extension == "kt" -> { + val text = FileUtil.loadFile(file, true) + val fileText = + if (text.startsWith("package")) + text + else + "package ${file.nameWithoutExtension};$text" + configureByText(file.name, fileText)!! + } + file.extension == "gradle" -> { + val text = FileUtil.loadFile(file, true) + val fileText = text.replace("\$PLUGIN_VERSION", bundledRuntimeVersion()) + configureByText(file.name, fileText)!! + } + else -> { + val filePath = file.relativeTo(srcDir).invariantSeparatorsPath + configureByFile(filePath) + } } }.toList() diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java index 5d5fde68b67..bc557eb8249 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java @@ -154,6 +154,12 @@ public class InspectionTestGenerated extends AbstractInspectionTest { doTest(fileName); } + @TestMetadata("gradleWrongPluginVersion/inspectionData/inspections.test") + public void testGradleWrongPluginVersion_inspectionData_Inspections_test() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/gradleWrongPluginVersion/inspectionData/inspections.test"); + doTest(fileName); + } + @TestMetadata("hasPlatformType/inspectionData/inspections.test") public void testHasPlatformType_inspectionData_Inspections_test() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/hasPlatformType/inspectionData/inspections.test");