Add intentions for registering Android components in manifest

Activity, Service, BroadcastReceiver

#KT-17389 Fixed
This commit is contained in:
Vyacheslav Gerasimov
2017-04-18 19:02:54 +03:00
parent 5b58a6f9e8
commit 06c8de02b5
57 changed files with 912 additions and 2 deletions
+1
View File
@@ -23,5 +23,6 @@
<orderEntry type="module" module-name="lint-idea" scope="TEST" />
<orderEntry type="library" name="uast-java" level="project" />
<orderEntry type="library" name="kotlin-reflect" level="project" />
<orderEntry type="module" module-name="light-classes" />
</component>
</module>
@@ -0,0 +1,54 @@
/*
* 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.intention
import com.intellij.openapi.editor.Editor
import org.jetbrains.android.dom.manifest.Manifest
import org.jetbrains.android.facet.AndroidFacet
import org.jetbrains.kotlin.android.insideBody
import org.jetbrains.kotlin.idea.intentions.SelfTargetingIntention
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.psiUtil.isAbstract
import org.jetbrains.kotlin.psi.psiUtil.isPrivate
import org.jetbrains.kotlin.psi.psiUtil.isProtected
abstract class AbstractRegisterComponentAction(text: String) : SelfTargetingIntention<KtClass>(KtClass::class.java, text) {
abstract fun isApplicableTo(element: KtClass, manifest: Manifest): Boolean
abstract fun applyTo(element: KtClass, manifest: Manifest)
final override fun isApplicableTo(element: KtClass, caretOffset: Int): Boolean {
val androidFacet = AndroidFacet.getInstance(element.containingFile) ?: return false
val manifest = androidFacet.manifest ?: return false
return !element.isLocal &&
!element.isAbstract() &&
!element.isPrivate() &&
!element.isProtected() &&
!element.isInner() &&
!element.name.isNullOrEmpty() &&
!element.insideBody(caretOffset) &&
isApplicableTo(element, manifest)
}
final override fun applyTo(element: KtClass, editor: Editor?) {
AndroidFacet.getInstance(element.containingFile)?.manifest?.let {
applyTo(element, it)
}
}
}
@@ -0,0 +1,45 @@
/*
* 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.intention
import com.android.SdkConstants
import org.jetbrains.android.dom.manifest.Manifest
import org.jetbrains.kotlin.android.isSubclassOf
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.asJava.toLightClass
class AddActivityToManifest : AbstractRegisterComponentAction("Add activity to manifest") {
override fun isApplicableTo(element: KtClass, manifest: Manifest): Boolean =
element.isSubclassOfActivity() && !element.isRegisteredActivity(manifest)
override fun applyTo(element: KtClass, manifest: Manifest) = runWriteAction {
val psiClass = element.toLightClass() ?: return@runWriteAction
manifest.application.addActivity().activityClass.value = psiClass
}
private fun KtClass.isRegisteredActivity(manifest: Manifest) = manifest.application.activities.any {
it.activityClass.value?.qualifiedName == fqName?.asString()
}
private fun KtClass.isSubclassOfActivity() =
(descriptor as? ClassDescriptor)?.defaultType?.isSubclassOf(FqName(SdkConstants.CLASS_ACTIVITY), true) ?: false
}
@@ -0,0 +1,45 @@
/*
* 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.intention
import com.android.SdkConstants
import org.jetbrains.android.dom.manifest.Manifest
import org.jetbrains.kotlin.android.isSubclassOf
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtClass
class AddBroadcastReceiverToManifest : AbstractRegisterComponentAction("Add broadcast receiver to manifest") {
override fun isApplicableTo(element: KtClass, manifest: Manifest): Boolean =
element.isSubclassOfBroadcastReceiver() && !element.isRegisteredBroadcastReceiver(manifest)
override fun applyTo(element: KtClass, manifest: Manifest) = runWriteAction {
val psiClass = element.toLightClass() ?: return@runWriteAction
manifest.application.addReceiver().receiverClass.value = psiClass
}
private fun KtClass.isRegisteredBroadcastReceiver(manifest: Manifest) = manifest.application.receivers.any {
it.receiverClass.value?.qualifiedName == fqName?.asString()
}
private fun KtClass.isSubclassOfBroadcastReceiver() =
(descriptor as? ClassDescriptor)?.defaultType?.isSubclassOf(FqName(SdkConstants.CLASS_BROADCASTRECEIVER), true) ?: false
}
@@ -0,0 +1,45 @@
/*
* 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.intention
import com.android.SdkConstants
import org.jetbrains.android.dom.manifest.Manifest
import org.jetbrains.kotlin.android.isSubclassOf
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtClass
class AddServiceToManifest : AbstractRegisterComponentAction("Add service to manifest") {
override fun isApplicableTo(element: KtClass, manifest: Manifest): Boolean =
element.isSubclassOfService() && !element.isRegisteredService(manifest)
override fun applyTo(element: KtClass, manifest: Manifest) = runWriteAction {
val psiClass = element.toLightClass() ?: return@runWriteAction
manifest.application.addService().serviceClass.value = psiClass
}
private fun KtClass.isRegisteredService(manifest: Manifest) = manifest.application.services.any {
it.serviceClass.value?.qualifiedName == fqName?.asString()
}
private fun KtClass.isSubclassOfService() =
(descriptor as? ClassDescriptor)?.defaultType?.isSubclassOf(FqName(SdkConstants.CLASS_SERVICE), true) ?: false
}
@@ -37,12 +37,19 @@ abstract class AbstractAndroidIntentionTest : KotlinAndroidTestCase() {
val notAvailable = InTextDirectivesUtils.isDirectiveDefined(testFileText, "// NOT_AVAILABLE")
val withRuntime = InTextDirectivesUtils.isDirectiveDefined(testFileText, "// WITH_RUNTIME")
val checkManifest = InTextDirectivesUtils.isDirectiveDefined(testFileText, "// CHECK_MANIFEST")
try {
if (withRuntime) {
ConfigLibraryUtil.configureKotlinRuntime(myFixture.module)
}
val customManifestPath = "${testFile.path}.AndroidManifest.xml"
if (FileUtil.exists("$testDataPath/$customManifestPath")) {
deleteManifest()
myFixture.copyFileToProject(customManifestPath, SdkConstants.FN_ANDROID_MANIFEST_XML)
}
val sourceFile = myFixture.copyFileToProject(path, "src/${PathUtil.getFileName(path)}")
myFixture.configureFromExistingVirtualFile(sourceFile)
@@ -55,8 +62,18 @@ abstract class AbstractAndroidIntentionTest : KotlinAndroidTestCase() {
error("Intention is not available!")
}
if (notAvailable) {
error("Intention should not be available!")
}
myFixture.launchAction(intention)
myFixture.checkResultByFile("$path.expected")
if (checkManifest) {
myFixture.checkResultByFile("AndroidManifest.xml", "$customManifestPath.expected", true)
}
else {
myFixture.checkResultByFile("$path.expected")
}
}
finally {
if (withRuntime) {
@@ -36,6 +36,213 @@ public class AndroidIntentionTestGenerated extends AbstractAndroidIntentionTest
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/android/intention"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("idea/testData/android/intention/addActivityToManifest")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class AddActivityToManifest extends AbstractAndroidIntentionTest {
@TestMetadata("abstract.kt")
public void testAbstract() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addActivityToManifest/abstract.kt");
doTest(fileName);
}
public void testAllFilesPresentInAddActivityToManifest() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/android/intention/addActivityToManifest"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("alreadyExists.kt")
public void testAlreadyExists() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addActivityToManifest/alreadyExists.kt");
doTest(fileName);
}
@TestMetadata("inner.kt")
public void testInner() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addActivityToManifest/inner.kt");
doTest(fileName);
}
@TestMetadata("insideBody.kt")
public void testInsideBody() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addActivityToManifest/insideBody.kt");
doTest(fileName);
}
@TestMetadata("local.kt")
public void testLocal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addActivityToManifest/local.kt");
doTest(fileName);
}
@TestMetadata("nested.kt")
public void testNested() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addActivityToManifest/nested.kt");
doTest(fileName);
}
@TestMetadata("notActivity.kt")
public void testNotActivity() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addActivityToManifest/notActivity.kt");
doTest(fileName);
}
@TestMetadata("private.kt")
public void testPrivate() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addActivityToManifest/private.kt");
doTest(fileName);
}
@TestMetadata("protected.kt")
public void testProtected() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addActivityToManifest/protected.kt");
doTest(fileName);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addActivityToManifest/simple.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/android/intention/addBroadcastReceiverToManifest")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class AddBroadcastReceiverToManifest extends AbstractAndroidIntentionTest {
@TestMetadata("abstract.kt")
public void testAbstract() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addBroadcastReceiverToManifest/abstract.kt");
doTest(fileName);
}
public void testAllFilesPresentInAddBroadcastReceiverToManifest() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/android/intention/addBroadcastReceiverToManifest"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("alreadyExists.kt")
public void testAlreadyExists() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addBroadcastReceiverToManifest/alreadyExists.kt");
doTest(fileName);
}
@TestMetadata("inner.kt")
public void testInner() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addBroadcastReceiverToManifest/inner.kt");
doTest(fileName);
}
@TestMetadata("insideBody.kt")
public void testInsideBody() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addBroadcastReceiverToManifest/insideBody.kt");
doTest(fileName);
}
@TestMetadata("local.kt")
public void testLocal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addBroadcastReceiverToManifest/local.kt");
doTest(fileName);
}
@TestMetadata("nested.kt")
public void testNested() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addBroadcastReceiverToManifest/nested.kt");
doTest(fileName);
}
@TestMetadata("notReceiver.kt")
public void testNotReceiver() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addBroadcastReceiverToManifest/notReceiver.kt");
doTest(fileName);
}
@TestMetadata("private.kt")
public void testPrivate() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addBroadcastReceiverToManifest/private.kt");
doTest(fileName);
}
@TestMetadata("protected.kt")
public void testProtected() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addBroadcastReceiverToManifest/protected.kt");
doTest(fileName);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addBroadcastReceiverToManifest/simple.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/android/intention/addServiceToManifest")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class AddServiceToManifest extends AbstractAndroidIntentionTest {
@TestMetadata("abstract.kt")
public void testAbstract() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addServiceToManifest/abstract.kt");
doTest(fileName);
}
public void testAllFilesPresentInAddServiceToManifest() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/android/intention/addServiceToManifest"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("alreadyExists.kt")
public void testAlreadyExists() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addServiceToManifest/alreadyExists.kt");
doTest(fileName);
}
@TestMetadata("inner.kt")
public void testInner() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addServiceToManifest/inner.kt");
doTest(fileName);
}
@TestMetadata("insideBody.kt")
public void testInsideBody() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addServiceToManifest/insideBody.kt");
doTest(fileName);
}
@TestMetadata("local.kt")
public void testLocal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addServiceToManifest/local.kt");
doTest(fileName);
}
@TestMetadata("nested.kt")
public void testNested() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addServiceToManifest/nested.kt");
doTest(fileName);
}
@TestMetadata("notService.kt")
public void testNotService() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addServiceToManifest/notService.kt");
doTest(fileName);
}
@TestMetadata("private.kt")
public void testPrivate() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addServiceToManifest/private.kt");
doTest(fileName);
}
@TestMetadata("protected.kt")
public void testProtected() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addServiceToManifest/protected.kt");
doTest(fileName);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/intention/addServiceToManifest/simple.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/android/intention/implementParcelable")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)