Implemented kotlin quick fix provider for create android resource quick fixes
#KT-10465 Fixed #KT-12880 Fixed
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
+58
@@ -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
|
||||
}
|
||||
|
||||
+27
-8
@@ -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")
|
||||
|
||||
+36
@@ -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");
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
+10
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"isApplicable": false,
|
||||
"intentionText": "Create color value resource 'super_green'"
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import android.app.Activity
|
||||
import com.myapp.R
|
||||
|
||||
fun Activity.getSuperGreenColor() = getColor(R.color.<caret>super_green)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.myapp;
|
||||
|
||||
public final class R {
|
||||
public static final class string {
|
||||
|
||||
}
|
||||
public static final class color {
|
||||
|
||||
}
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import android.app.Activity
|
||||
import com.myapp.R
|
||||
|
||||
fun Activity.getSuperGreenColor() = getColor(R.color.super_green)
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="super_green">a</color>
|
||||
</resources>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<resources>
|
||||
</resources>
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import android.app.Activity
|
||||
import com.myapp.R
|
||||
|
||||
fun Activity.getSuperGreenColor() = getColor(R.color.<caret>super_green)
|
||||
idea/testData/android/resourceIntentions/createColorValueResource/simpleFunction/simpleFunction.test
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"resDirectory": "../../res",
|
||||
"intentionText": "Create color value resource 'super_green'"
|
||||
}
|
||||
+10
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"isApplicable": false,
|
||||
"intentionText": "Create layout resource file 'activity_main.xml'"
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import android.app.Activity
|
||||
import com.myapp.R
|
||||
|
||||
fun Activity.getSetMainContentView() = setContentView(R.layout.<caret>activity_main)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.myapp;
|
||||
|
||||
public final class R {
|
||||
public static final class string {
|
||||
|
||||
}
|
||||
public static final class layout {
|
||||
|
||||
}
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import android.app.Activity
|
||||
import com.myapp.R
|
||||
|
||||
fun Activity.getSetMainContentView() = setContentView(R.layout.activity_main)
|
||||
+7
@@ -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>
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<resources>
|
||||
</resources>
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import android.app.Activity
|
||||
import com.myapp.R
|
||||
|
||||
fun Activity.getSetMainContentView() = setContentView(R.layout.<caret>activity_main)
|
||||
idea/testData/android/resourceIntentions/createLayoutResourceFile/simpleFunction/simpleFunction.test
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"resDirectory": "../../res",
|
||||
"intentionText": "Create layout resource file 'activity_main.xml'"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.myapp;
|
||||
|
||||
public final class R {
|
||||
public static final class string {
|
||||
public static final int my_activity_title = 0x7f060021;
|
||||
}
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"isApplicable": false,
|
||||
"intentionText": "Create string value resource 'my_activity_title'"
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import android.content.Context
|
||||
import com.myapp.R
|
||||
|
||||
fun Context.getActivityTitle() = getString(R.string.<caret>my_activity_title)
|
||||
|
||||
+8
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import android.content.Context
|
||||
import com.myapp.R
|
||||
|
||||
fun Context.getActivityTitle() = getString(R.string.<caret>my_activity_title)
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="my_activity_title">a</string>
|
||||
</resources>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import android.content.Context
|
||||
import com.myapp.R
|
||||
|
||||
fun Context.getActivityTitle() = getString(R.string.<caret>my_activity_title)
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"resDirectory": "../../res",
|
||||
"intentionText": "Create string value resource 'my_activity_title'"
|
||||
}
|
||||
Reference in New Issue
Block a user