Implemented kotlin quick fix provider for create android resource quick fixes

#KT-10465 Fixed
#KT-12880 Fixed
This commit is contained in:
Vyacheslav Gerasimov
2016-11-03 13:56:02 +03:00
parent 29b9521596
commit 9534934f43
32 changed files with 327 additions and 11 deletions
@@ -16,10 +16,73 @@
package org.jetbrains.kotlin.android
import com.android.SdkConstants
import com.intellij.psi.PsiElement
import org.jetbrains.android.augment.AndroidPsiElementFinder
import org.jetbrains.android.facet.AndroidFacet
import org.jetbrains.android.util.AndroidResourceUtil
import org.jetbrains.android.util.AndroidResourceUtil.isManifestJavaFile
import org.jetbrains.android.util.AndroidResourceUtil.isRJavaFile
import org.jetbrains.android.util.AndroidUtils
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelector
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.resolve.source.PsiSourceFile
fun PsiElement.getAndroidFacetForFile(): AndroidFacet? {
internal fun PsiElement.getAndroidFacetForFile(): AndroidFacet? {
val file = containingFile ?: return null
return AndroidFacet.getInstance(file)
}
internal object KotlinAndroidResourceUtil {
fun getReferredResourceOrManifestField(facet: AndroidFacet, expression: KtSimpleNameExpression, localOnly: Boolean)
= getReferredResourceOrManifestField(facet, expression, null, localOnly)
fun getReferredResourceOrManifestField(facet: AndroidFacet, expression: KtSimpleNameExpression,
className: String?, localOnly: Boolean): AndroidResourceUtil.MyReferredResourceFieldInfo? {
val resFieldName = expression.getReferencedName()
val resClassReference = expression.getPreviousInQualifiedChain() as? KtSimpleNameExpression ?: return null
val resClassName = resClassReference.getReferencedName()
if (resClassName.isEmpty() || className != null && className != resClassName) {
return null
}
val rClassReference = resClassReference.getPreviousInQualifiedChain() as? KtSimpleNameExpression ?: return null
val rClassDescriptor = rClassReference.analyze(BodyResolveMode.PARTIAL)
.get(BindingContext.REFERENCE_TARGET, rClassReference) as? ClassDescriptor ?: return null
val rClassShortName = rClassDescriptor.name.asString()
val fromManifest = AndroidUtils.MANIFEST_CLASS_NAME == rClassShortName
if (!fromManifest && AndroidUtils.R_CLASS_NAME != rClassShortName) {
return null
}
if (!localOnly) {
val qName = rClassDescriptor.fqNameSafe.asString()
if (SdkConstants.CLASS_R == qName || AndroidPsiElementFinder.INTERNAL_R_CLASS_QNAME == qName) {
return AndroidResourceUtil.MyReferredResourceFieldInfo(resClassName, resFieldName, true, false)
}
}
val containingFile = (rClassDescriptor.source.containingFile as? PsiSourceFile)?.psiFile ?: return null
if (if (fromManifest) !isManifestJavaFile(facet, containingFile) else !isRJavaFile(facet, containingFile)) {
return null
}
return AndroidResourceUtil.MyReferredResourceFieldInfo(resClassName, resFieldName, false, false)
}
private fun KtExpression.getPreviousInQualifiedChain(): KtExpression? {
val receiverExpression = getQualifiedExpressionForSelector()?.receiverExpression
return (receiverExpression as? KtQualifiedExpression)?.selectorExpression ?: receiverExpression
}
}
@@ -0,0 +1,58 @@
/*
* 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.android.inspection
import com.android.resources.ResourceType
import com.intellij.codeInsight.daemon.QuickFixActionRegistrar
import com.intellij.codeInsight.quickfix.UnresolvedReferenceQuickFixProvider
import com.intellij.openapi.module.ModuleUtil
import org.jetbrains.android.facet.AndroidFacet
import org.jetbrains.android.inspections.CreateFileResourceQuickFix
import org.jetbrains.android.inspections.CreateValueResourceQuickFix
import org.jetbrains.android.util.AndroidResourceUtil
import org.jetbrains.kotlin.android.KotlinAndroidResourceUtil
import org.jetbrains.kotlin.idea.references.KtSimpleNameReference
class KotlinAndroidResourceQuickFixProvider : UnresolvedReferenceQuickFixProvider<KtSimpleNameReference>() {
override fun registerFixes(ref: KtSimpleNameReference, registrar: QuickFixActionRegistrar) {
val expression = ref.expression
val contextModule = ModuleUtil.findModuleForPsiElement(expression) ?: return
val facet = AndroidFacet.getInstance(contextModule) ?: return
val manifest = facet.manifest ?: return
manifest.`package`.value ?: return
val contextFile = expression.containingFile ?: return
val info = KotlinAndroidResourceUtil.getReferredResourceOrManifestField(facet, expression, true)
if (info == null || info.isFromManifest) {
return
}
val resourceType = ResourceType.getEnum(info.className)
if (AndroidResourceUtil.ALL_VALUE_RESOURCE_TYPES.contains(resourceType)) {
registrar.register(CreateValueResourceQuickFix(facet, resourceType, info.fieldName, contextFile, true))
}
if (AndroidResourceUtil.XML_FILE_RESOURCE_TYPES.contains(resourceType)) {
registrar.register(CreateFileResourceQuickFix(facet, resourceType, info.fieldName, contextFile, true))
}
}
override fun getReferenceClass() = KtSimpleNameReference::class.java
}
@@ -28,13 +28,15 @@ import com.intellij.util.PathUtil
import junit.framework.TestCase
import org.jetbrains.kotlin.android.KotlinAndroidTestCase
import org.jetbrains.kotlin.idea.jsonUtils.getString
import org.jetbrains.kotlin.idea.test.DirectiveBasedActionUtils
import org.jetbrains.kotlin.psi.KtFile
import java.io.File
abstract class AbstractAndroidResourceIntentionTest : KotlinAndroidTestCase() {
companion object {
val COM_MYAPP_PACKAGE_PATH = "com/myapp/"
}
fun doTest(path: String) {
val configFile = File(path)
val testDataPath = configFile.parent
@@ -43,17 +45,18 @@ abstract class AbstractAndroidResourceIntentionTest : KotlinAndroidTestCase() {
val config = JsonParser().parse(FileUtil.loadFile(File(path), true)) as JsonObject
val intentionClass = config.getString("intentionClass")
val intentionClass = if (config.has("intentionClass")) config.getString("intentionClass") else null
val intentionText = if (config.has("intentionText")) config.getString("intentionText") else null
val isApplicableExpected = if (config.has("isApplicable")) config.get("isApplicable").asBoolean else true
val rFile = if (config.has("rFile")) config.get("rFile").asString else null
val resDirectory = if (config.has("resDirectory")) config.get("resDirectory").asString else null
if (rFile != null) {
myFixture.copyFileToProject(rFile, "gen/" + PathUtil.getFileName(rFile))
myFixture.copyFileToProject(rFile, "gen/$COM_MYAPP_PACKAGE_PATH" + PathUtil.getFileName(rFile))
}
else {
if (File(testDataPath + "/R.java").isFile) {
myFixture.copyFileToProject("R.java", "gen/R.java")
myFixture.copyFileToProject("R.java", "gen/${COM_MYAPP_PACKAGE_PATH}R.java")
}
}
@@ -69,9 +72,26 @@ abstract class AbstractAndroidResourceIntentionTest : KotlinAndroidTestCase() {
val sourceFile = myFixture.copyFileToProject("main.kt", "src/main.kt")
myFixture.configureFromExistingVirtualFile(sourceFile)
DirectiveBasedActionUtils.checkForUnexpectedErrors(myFixture.file as KtFile)
val intentionAction: IntentionAction?
if (intentionClass != null) {
intentionAction = Class.forName(intentionClass).newInstance() as IntentionAction
}
else if (intentionText != null) {
intentionAction = myFixture.getAvailableIntention(intentionText)
if (intentionAction != null && !isApplicableExpected) {
TestCase.fail("Intention action should not be available")
}
}
else {
intentionAction = null
}
val intentionAction = Class.forName(intentionClass).newInstance() as IntentionAction
if (intentionAction == null) {
if (isApplicableExpected) {
TestCase.fail("Couldn't find intention action")
}
return
}
TestCase.assertEquals(isApplicableExpected, intentionAction.isAvailable(myFixture.project, myFixture.editor, myFixture.file))
if (!isApplicableExpected) {
@@ -84,7 +104,6 @@ abstract class AbstractAndroidResourceIntentionTest : KotlinAndroidTestCase() {
myFixture.launchAction(intentionAction)
FileDocumentManager.getInstance().saveAllDocuments()
DirectiveBasedActionUtils.checkForUnexpectedErrors(myFixture.file as KtFile)
myFixture.checkResultByFile("/expected/main.kt")
assertResourcesEqual(testDataPath + "/expected/res")
@@ -35,6 +35,42 @@ public class AndroidResourceIntentionTestGenerated extends AbstractAndroidResour
KotlinTestUtils.assertAllTestsPresentInSingleGeneratedClass(this.getClass(), new File("idea/testData/android/resourceIntentions"), Pattern.compile("^(.+)\\.test$"));
}
@TestMetadata("createColorValueResource/alreadyExists/alreadyExists.test")
public void testCreateColorValueResource_alreadyExists_AlreadyExists() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/resourceIntentions/createColorValueResource/alreadyExists/alreadyExists.test");
doTest(fileName);
}
@TestMetadata("createColorValueResource/simpleFunction/simpleFunction.test")
public void testCreateColorValueResource_simpleFunction_SimpleFunction() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/resourceIntentions/createColorValueResource/simpleFunction/simpleFunction.test");
doTest(fileName);
}
@TestMetadata("createLayoutResourceFile/alreadyExists/alreadyExists.test")
public void testCreateLayoutResourceFile_alreadyExists_AlreadyExists() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/resourceIntentions/createLayoutResourceFile/alreadyExists/alreadyExists.test");
doTest(fileName);
}
@TestMetadata("createLayoutResourceFile/simpleFunction/simpleFunction.test")
public void testCreateLayoutResourceFile_simpleFunction_SimpleFunction() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/resourceIntentions/createLayoutResourceFile/simpleFunction/simpleFunction.test");
doTest(fileName);
}
@TestMetadata("createStringValueResource/alreadyExists/alreadyExists.test")
public void testCreateStringValueResource_alreadyExists_AlreadyExists() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/resourceIntentions/createStringValueResource/alreadyExists/alreadyExists.test");
doTest(fileName);
}
@TestMetadata("createStringValueResource/simpleFunction/simpleFunction.test")
public void testCreateStringValueResource_simpleFunction_SimpleFunction() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/resourceIntentions/createStringValueResource/simpleFunction/simpleFunction.test");
doTest(fileName);
}
@TestMetadata("kotlinAndroidAddStringResource/activityExtension/activityExtension.test")
public void testKotlinAndroidAddStringResource_activityExtension_ActivityExtension() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/resourceIntentions/kotlinAndroidAddStringResource/activityExtension/activityExtension.test");
+4
View File
@@ -23,6 +23,10 @@
<category>Kotlin Android</category>
</intentionAction>
<codeInsight.unresolvedReferenceQuickFixProvider
implementation="org.jetbrains.kotlin.android.inspection.KotlinAndroidResourceQuickFixProvider"/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains.kotlin">
@@ -25,9 +25,9 @@ import com.intellij.openapi.util.TextRange
import com.intellij.psi.SmartPsiElementPointer
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.idea.core.quickfix.QuickFixUtil
import org.jetbrains.kotlin.idea.quickfix.IntentionActionPriority
import org.jetbrains.kotlin.idea.quickfix.KotlinIntentionActionFactoryWithDelegate
import org.jetbrains.kotlin.idea.quickfix.QuickFixWithDelegateFactory
import org.jetbrains.kotlin.idea.quickfix.detectPriority
import org.jetbrains.kotlin.idea.references.KtSimpleNameReference
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import java.util.*
@@ -42,7 +42,7 @@ object PlatformUnresolvedProvider : KotlinIntentionActionFactoryWithDelegate<KtN
originalElementPointer.element?.references?.filterIsInstance<KtSimpleNameReference>()?.firstOrNull()?.let { reference ->
UnresolvedReferenceQuickFixProvider.registerReferenceFixes(reference, object: QuickFixActionRegistrar {
override fun register(action: IntentionAction) {
result.add(QuickFixWithDelegateFactory(IntentionActionPriority.LOW) { action })
result.add(QuickFixWithDelegateFactory(action.detectPriority()) { action })
}
override fun register(fixRange: TextRange, action: IntentionAction, key: HighlightDisplayKey?) {
@@ -0,0 +1,10 @@
package com.myapp;
public final class R {
public static final class string {
}
public static final class color {
public static final int super_green = 0x7f060021;
}
}
@@ -0,0 +1,4 @@
{
"isApplicable": false,
"intentionText": "Create color value resource 'super_green'"
}
@@ -0,0 +1,4 @@
import android.app.Activity
import com.myapp.R
fun Activity.getSuperGreenColor() = getColor(R.color.<caret>super_green)
@@ -0,0 +1,10 @@
package com.myapp;
public final class R {
public static final class string {
}
public static final class color {
}
}
@@ -0,0 +1,4 @@
import android.app.Activity
import com.myapp.R
fun Activity.getSuperGreenColor() = getColor(R.color.super_green)
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="super_green">a</color>
</resources>
@@ -0,0 +1,2 @@
<resources>
</resources>
@@ -0,0 +1,4 @@
import android.app.Activity
import com.myapp.R
fun Activity.getSuperGreenColor() = getColor(R.color.<caret>super_green)
@@ -0,0 +1,4 @@
{
"resDirectory": "../../res",
"intentionText": "Create color value resource 'super_green'"
}
@@ -0,0 +1,10 @@
package com.myapp;
public final class R {
public static final class string {
}
public static final class layout {
public static final int activity_main = 0x7f060021;
}
}
@@ -0,0 +1,4 @@
{
"isApplicable": false,
"intentionText": "Create layout resource file 'activity_main.xml'"
}
@@ -0,0 +1,4 @@
import android.app.Activity
import com.myapp.R
fun Activity.getSetMainContentView() = setContentView(R.layout.<caret>activity_main)
@@ -0,0 +1,10 @@
package com.myapp;
public final class R {
public static final class string {
}
public static final class layout {
}
}
@@ -0,0 +1,4 @@
import android.app.Activity
import com.myapp.R
fun Activity.getSetMainContentView() = setContentView(R.layout.activity_main)
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
@@ -0,0 +1,2 @@
<resources>
</resources>
@@ -0,0 +1,4 @@
import android.app.Activity
import com.myapp.R
fun Activity.getSetMainContentView() = setContentView(R.layout.<caret>activity_main)
@@ -0,0 +1,4 @@
{
"resDirectory": "../../res",
"intentionText": "Create layout resource file 'activity_main.xml'"
}
@@ -0,0 +1,7 @@
package com.myapp;
public final class R {
public static final class string {
public static final int my_activity_title = 0x7f060021;
}
}
@@ -0,0 +1,4 @@
{
"isApplicable": false,
"intentionText": "Create string value resource 'my_activity_title'"
}
@@ -0,0 +1,5 @@
import android.content.Context
import com.myapp.R
fun Context.getActivityTitle() = getString(R.string.<caret>my_activity_title)
@@ -0,0 +1,8 @@
package com.myapp;
public final class R {
public static final class string {
public static final int app_name = 0x7f060021;
public static final int resource_id = 0x7f060022;
}
}
@@ -0,0 +1,5 @@
import android.content.Context
import com.myapp.R
fun Context.getActivityTitle() = getString(R.string.<caret>my_activity_title)
@@ -0,0 +1,3 @@
<resources>
<string name="my_activity_title">a</string>
</resources>
@@ -0,0 +1,5 @@
import android.content.Context
import com.myapp.R
fun Context.getActivityTitle() = getString(R.string.<caret>my_activity_title)
@@ -0,0 +1,4 @@
{
"resDirectory": "../../res",
"intentionText": "Create string value resource 'my_activity_title'"
}