Android: Add intention to generate View constructor convention

Fixes #KT-15150
This commit is contained in:
Kirill Rakhman
2017-01-16 23:06:55 +01:00
committed by Vyacheslav Gerasimov
parent f74c0950d2
commit ca86dbee72
14 changed files with 258 additions and 0 deletions
@@ -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)
}
}
@@ -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<KtSuperTypeEntry>(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<KtClass>(
"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<List<String?>>? {
val classDescriptor = constructor.declarationDescriptor as? ClassDescriptor ?: return null
return classDescriptor.constructors.map {
it.valueParameters.map { it.type.getFqNameAsString() }
}
}
}
}
@@ -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);
}
}
}
+1
View File
@@ -38,6 +38,7 @@
<simpleNameReferenceExtension implementation="org.jetbrains.kotlin.android.synthetic.idea.AndroidSimpleNameReferenceExtension"/>
<kotlinIndicesHelperExtension implementation="org.jetbrains.kotlin.android.synthetic.idea.AndroidIndicesHelperExtension"/>
<quickFixContributor implementation="org.jetbrains.kotlin.android.quickfix.AndroidQuickFixRegistrar"/>
<projectConfigurator implementation="org.jetbrains.kotlin.android.configure.KotlinAndroidGradleModuleConfigurator"/>
</extensions>
@@ -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<caret>(context, attrs, defStyleAttr)
@@ -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)
}
@@ -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
@@ -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
@@ -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<caret>
@@ -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<caret>(context, attrs, defStyleAttr)
@@ -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)
}
@@ -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
@@ -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
@@ -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<caret>