From ca86dbee72720f03f5f27f73d1930ed7c09978a7 Mon Sep 17 00:00:00 2001 From: Kirill Rakhman Date: Mon, 16 Jan 2017 23:06:55 +0100 Subject: [PATCH] Android: Add intention to generate View constructor convention Fixes #KT-15150 --- .../quickfix/AndroidQuickFixRegistrar.kt | 27 +++++ .../KotlinAndroidViewConstructorFix.kt | 109 ++++++++++++++++++ ...AndroidQuickFixMultiFileTestGenerated.java | 21 ++++ idea/src/META-INF/android.xml | 1 + .../viewConstructor/indirect.after.kt | 13 +++ .../indirect.before.Dependency1.kt | 19 +++ .../indirect.before.Dependency2.kt | 6 + .../indirect.before.Dependency3.kt | 6 + .../viewConstructor/indirect.before.Main.kt | 9 ++ .../quickfix/viewConstructor/simple.after.kt | 13 +++ .../simple.before.Dependency1.kt | 13 +++ .../simple.before.Dependency2.kt | 6 + .../simple.before.Dependency3.kt | 6 + .../viewConstructor/simple.before.Main.kt | 9 ++ 14 files changed, 258 insertions(+) create mode 100644 idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/AndroidQuickFixRegistrar.kt create mode 100644 idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/KotlinAndroidViewConstructorFix.kt create mode 100644 idea/testData/android/quickfix/viewConstructor/indirect.after.kt create mode 100644 idea/testData/android/quickfix/viewConstructor/indirect.before.Dependency1.kt create mode 100644 idea/testData/android/quickfix/viewConstructor/indirect.before.Dependency2.kt create mode 100644 idea/testData/android/quickfix/viewConstructor/indirect.before.Dependency3.kt create mode 100644 idea/testData/android/quickfix/viewConstructor/indirect.before.Main.kt create mode 100644 idea/testData/android/quickfix/viewConstructor/simple.after.kt create mode 100644 idea/testData/android/quickfix/viewConstructor/simple.before.Dependency1.kt create mode 100644 idea/testData/android/quickfix/viewConstructor/simple.before.Dependency2.kt create mode 100644 idea/testData/android/quickfix/viewConstructor/simple.before.Dependency3.kt create mode 100644 idea/testData/android/quickfix/viewConstructor/simple.before.Main.kt diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/AndroidQuickFixRegistrar.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/AndroidQuickFixRegistrar.kt new file mode 100644 index 00000000000..e7a53b57f45 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/AndroidQuickFixRegistrar.kt @@ -0,0 +1,27 @@ +/* + * 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.android.quickfix + +import org.jetbrains.kotlin.diagnostics.Errors.SUPERTYPE_NOT_INITIALIZED +import org.jetbrains.kotlin.idea.quickfix.QuickFixContributor +import org.jetbrains.kotlin.idea.quickfix.QuickFixes + +class AndroidQuickFixRegistrar : QuickFixContributor { + override fun registerQuickFixes(quickFixes: QuickFixes) { + quickFixes.register(SUPERTYPE_NOT_INITIALIZED, KotlinAndroidViewConstructorFix) + } +} \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/KotlinAndroidViewConstructorFix.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/KotlinAndroidViewConstructorFix.kt new file mode 100644 index 00000000000..a0c7ed9d253 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/KotlinAndroidViewConstructorFix.kt @@ -0,0 +1,109 @@ +/* + * 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.android.quickfix + +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import org.jetbrains.android.facet.AndroidFacet +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.diagnostics.Errors.SUPERTYPE_NOT_INITIALIZED +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.core.ShortenReferences +import org.jetbrains.kotlin.idea.core.replaced +import org.jetbrains.kotlin.idea.quickfix.KotlinQuickFixAction +import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory +import org.jetbrains.kotlin.idea.util.addAnnotation +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.containingClass +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.typeUtil.supertypes + +class KotlinAndroidViewConstructorFix(element: KtSuperTypeEntry) : KotlinQuickFixAction(element) { + + override fun getText() = "Add Android View constructors using '@JvmOverloads'" + override fun getFamilyName() = text + + override fun isAvailable(project: Project, editor: Editor?, file: PsiFile): Boolean { + if (AndroidFacet.getInstance(file) == null) return false + return super.isAvailable(project, editor, file) + } + + override fun invoke(project: Project, editor: Editor?, file: KtFile) { + val element = element ?: return + val ktClass = element.containingClass() ?: return + + val factory = KtPsiFactory(element) + + val newPrimaryConstructor = factory.createDeclarationByPattern( + "class A constructor(\n $0, $1, $2\n)", + factory.createParameter("context: android.content.Context"), + factory.createParameter("attrs: android.util.AttributeSet? = null"), + factory.createParameter("defStyleAttr: Int = 0") + ).primaryConstructor ?: return + + val primaryConstructor = ktClass.createPrimaryConstructorIfAbsent().replaced(newPrimaryConstructor) + primaryConstructor.valueParameterList?.let { ShortenReferences.DEFAULT.process(it) } + + primaryConstructor.addAnnotation(fqNameAnnotation, whiteSpaceText = " ") + + element.replace(factory.createSuperTypeCallEntry(element.text + "(context, attrs, defStyleAttr)")) + } + + companion object Factory : KotlinSingleIntentionActionFactory() { + + private val fqNameAnnotation = FqName("kotlin.jvm.JvmOverloads") + + private val requiredConstructorParameterTypes = + listOf("android.content.Context", "android.util.AttributeSet", "kotlin.Int") + + override fun createAction(diagnostic: Diagnostic): IntentionAction? { + val superTypeEntry = SUPERTYPE_NOT_INITIALIZED.cast(diagnostic).psiElement + + val ktClass = superTypeEntry.containingClass() ?: return null + if (ktClass.primaryConstructor != null) return null + + val context = superTypeEntry.analyze() + val type = superTypeEntry.typeReference?.let { context[BindingContext.TYPE, it] } ?: return null + + if (!type.isAndroidView() && type.supertypes().none { it.isAndroidView() }) return null + + val names = type.constructorParameters() ?: return null + if (requiredConstructorParameterTypes !in names) return null + + return KotlinAndroidViewConstructorFix(superTypeEntry) + } + + private fun KotlinType.getFqNameAsString() = constructor.declarationDescriptor?.fqNameUnsafe?.asString() + + private fun KotlinType.isAndroidView() = getFqNameAsString() == "android.view.View" + + private fun KotlinType.constructorParameters(): List>? { + val classDescriptor = constructor.declarationDescriptor as? ClassDescriptor ?: return null + return classDescriptor.constructors.map { + it.valueParameters.map { it.type.getFqNameAsString() } + } + } + } +} + + diff --git a/idea/idea-android/tests/org/jetbrains/kotlin/android/quickfixes/AndroidQuickFixMultiFileTestGenerated.java b/idea/idea-android/tests/org/jetbrains/kotlin/android/quickfixes/AndroidQuickFixMultiFileTestGenerated.java index c76e91ac6d7..ceed89548ce 100644 --- a/idea/idea-android/tests/org/jetbrains/kotlin/android/quickfixes/AndroidQuickFixMultiFileTestGenerated.java +++ b/idea/idea-android/tests/org/jetbrains/kotlin/android/quickfixes/AndroidQuickFixMultiFileTestGenerated.java @@ -50,4 +50,25 @@ public class AndroidQuickFixMultiFileTestGenerated extends AbstractAndroidQuickF doTestWithExtraFile(fileName); } } + + @TestMetadata("idea/testData/android/quickfix/viewConstructor") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ViewConstructor extends AbstractAndroidQuickFixMultiFileTest { + public void testAllFilesPresentInViewConstructor() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/android/quickfix/viewConstructor"), Pattern.compile("^(\\w+)\\.((before\\.Main\\.\\w+)|(test))$"), TargetBackend.ANY, true); + } + + @TestMetadata("indirect.before.Main.kt") + public void testIndirect() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/quickfix/viewConstructor/indirect.before.Main.kt"); + doTestWithExtraFile(fileName); + } + + @TestMetadata("simple.before.Main.kt") + public void testSimple() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/quickfix/viewConstructor/simple.before.Main.kt"); + doTestWithExtraFile(fileName); + } + } } diff --git a/idea/src/META-INF/android.xml b/idea/src/META-INF/android.xml index ebc3aebd016..19692f6d7f8 100644 --- a/idea/src/META-INF/android.xml +++ b/idea/src/META-INF/android.xml @@ -38,6 +38,7 @@ + diff --git a/idea/testData/android/quickfix/viewConstructor/indirect.after.kt b/idea/testData/android/quickfix/viewConstructor/indirect.after.kt new file mode 100644 index 00000000000..c4a1068612f --- /dev/null +++ b/idea/testData/android/quickfix/viewConstructor/indirect.after.kt @@ -0,0 +1,13 @@ +// "Add Android View constructors using '@JvmOverloads'" "true" +// ERROR: This type has a constructor, and thus must be initialized here +// WITH_RUNTIME + +package com.myapp.activity + +import android.content.Context +import android.util.AttributeSet +import android.view.TextView + +class Foo @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : TextView(context, attrs, defStyleAttr) \ No newline at end of file diff --git a/idea/testData/android/quickfix/viewConstructor/indirect.before.Dependency1.kt b/idea/testData/android/quickfix/viewConstructor/indirect.before.Dependency1.kt new file mode 100644 index 00000000000..a75485c7233 --- /dev/null +++ b/idea/testData/android/quickfix/viewConstructor/indirect.before.Dependency1.kt @@ -0,0 +1,19 @@ +// "Add Android View constructors using '@JvmOverloads'" "true" +// ERROR: The type has a constructor, and thus must be initialized here + +package android.view + +import android.util.AttributeSet +import android.content.Context + +public open class View { + constructor(context: Context) + constructor(context: Context, attrs: AttributeSet?) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) +} + +public open class TextView : View { + constructor(context: Context) super(context) + constructor(context: Context, attrs: AttributeSet?) super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) super(context, attrs, defStyleAttr) +} \ No newline at end of file diff --git a/idea/testData/android/quickfix/viewConstructor/indirect.before.Dependency2.kt b/idea/testData/android/quickfix/viewConstructor/indirect.before.Dependency2.kt new file mode 100644 index 00000000000..f9cd3d17f2d --- /dev/null +++ b/idea/testData/android/quickfix/viewConstructor/indirect.before.Dependency2.kt @@ -0,0 +1,6 @@ +// "Add Android View constructors using '@JvmOverloads'" "true" +// ERROR: The type has a constructor, and thus must be initialized here + +package android.content + +public class Context \ No newline at end of file diff --git a/idea/testData/android/quickfix/viewConstructor/indirect.before.Dependency3.kt b/idea/testData/android/quickfix/viewConstructor/indirect.before.Dependency3.kt new file mode 100644 index 00000000000..1f957f82d4d --- /dev/null +++ b/idea/testData/android/quickfix/viewConstructor/indirect.before.Dependency3.kt @@ -0,0 +1,6 @@ +// "Add Android View constructors using '@JvmOverloads'" "true" +// ERROR: The type has a constructor, and thus must be initialized here + +package android.util + +public class AttributeSet \ No newline at end of file diff --git a/idea/testData/android/quickfix/viewConstructor/indirect.before.Main.kt b/idea/testData/android/quickfix/viewConstructor/indirect.before.Main.kt new file mode 100644 index 00000000000..1e6d65ffdc6 --- /dev/null +++ b/idea/testData/android/quickfix/viewConstructor/indirect.before.Main.kt @@ -0,0 +1,9 @@ +// "Add Android View constructors using '@JvmOverloads'" "true" +// ERROR: This type has a constructor, and thus must be initialized here +// WITH_RUNTIME + +package com.myapp.activity + +import android.view.TextView + +class Foo : TextView \ No newline at end of file diff --git a/idea/testData/android/quickfix/viewConstructor/simple.after.kt b/idea/testData/android/quickfix/viewConstructor/simple.after.kt new file mode 100644 index 00000000000..82315d1a7fb --- /dev/null +++ b/idea/testData/android/quickfix/viewConstructor/simple.after.kt @@ -0,0 +1,13 @@ +// "Add Android View constructors using '@JvmOverloads'" "true" +// ERROR: This type has a constructor, and thus must be initialized here +// WITH_RUNTIME + +package com.myapp.activity + +import android.content.Context +import android.util.AttributeSet +import android.view.View + +class Foo @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) \ No newline at end of file diff --git a/idea/testData/android/quickfix/viewConstructor/simple.before.Dependency1.kt b/idea/testData/android/quickfix/viewConstructor/simple.before.Dependency1.kt new file mode 100644 index 00000000000..a212d1a70e3 --- /dev/null +++ b/idea/testData/android/quickfix/viewConstructor/simple.before.Dependency1.kt @@ -0,0 +1,13 @@ +// "Add Android View constructors using '@JvmOverloads'" "true" +// ERROR: The type has a constructor, and thus must be initialized here + +package android.view + +import android.util.AttributeSet +import android.content.Context + +public open class View { + constructor(context: Context) + constructor(context: Context, attrs: AttributeSet?) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) +} \ No newline at end of file diff --git a/idea/testData/android/quickfix/viewConstructor/simple.before.Dependency2.kt b/idea/testData/android/quickfix/viewConstructor/simple.before.Dependency2.kt new file mode 100644 index 00000000000..f9cd3d17f2d --- /dev/null +++ b/idea/testData/android/quickfix/viewConstructor/simple.before.Dependency2.kt @@ -0,0 +1,6 @@ +// "Add Android View constructors using '@JvmOverloads'" "true" +// ERROR: The type has a constructor, and thus must be initialized here + +package android.content + +public class Context \ No newline at end of file diff --git a/idea/testData/android/quickfix/viewConstructor/simple.before.Dependency3.kt b/idea/testData/android/quickfix/viewConstructor/simple.before.Dependency3.kt new file mode 100644 index 00000000000..1f957f82d4d --- /dev/null +++ b/idea/testData/android/quickfix/viewConstructor/simple.before.Dependency3.kt @@ -0,0 +1,6 @@ +// "Add Android View constructors using '@JvmOverloads'" "true" +// ERROR: The type has a constructor, and thus must be initialized here + +package android.util + +public class AttributeSet \ No newline at end of file diff --git a/idea/testData/android/quickfix/viewConstructor/simple.before.Main.kt b/idea/testData/android/quickfix/viewConstructor/simple.before.Main.kt new file mode 100644 index 00000000000..d4c51dc69e6 --- /dev/null +++ b/idea/testData/android/quickfix/viewConstructor/simple.before.Main.kt @@ -0,0 +1,9 @@ +// "Add Android View constructors using '@JvmOverloads'" "true" +// ERROR: This type has a constructor, and thus must be initialized here +// WITH_RUNTIME + +package com.myapp.activity + +import android.view.View + +class Foo : View \ No newline at end of file