From 5220dfc0ada31f1d912809409cc5a11397e31d2e Mon Sep 17 00:00:00 2001 From: Vyacheslav Gerasimov Date: Wed, 30 Aug 2017 18:19:58 +0300 Subject: [PATCH] Android: Add inspection & quickfix to convert findViewById to api 26 #KT-19940 Fixed Target Version 1.1.5 --- .../jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt | 9 +++ download_android_sdk.xml | 2 +- .../TypeParameterFindViewByIdInspection.kt | 79 +++++++++++++++++++ .../jetbrains/kotlin/android/TestUtils.java | 2 +- .../android/lint/AbstractKotlinLintTest.kt | 5 +- .../android/lint/KotlinLintTestGenerated.java | 6 ++ .../AndroidLintQuickfixTestGenerated.java | 21 +++++ .../TypeParameterFindViewById.html | 5 ++ idea/src/META-INF/android.xml | 10 ++- idea/testData/android/lint/findViewById.kt | 49 ++++++++++++ idea/testData/android/lint/viewHolder.kt | 16 ++-- .../lintQuickfix/findViewById/nullableType.kt | 28 +++++++ .../findViewById/nullableType.kt.expected | 28 +++++++ .../lintQuickfix/findViewById/simple.kt | 28 +++++++ .../findViewById/simple.kt.expected | 28 +++++++ 15 files changed, 303 insertions(+), 13 deletions(-) create mode 100644 idea/idea-android/src/org/jetbrains/kotlin/android/inspection/TypeParameterFindViewByIdInspection.kt create mode 100644 idea/resources/inspectionDescriptions/TypeParameterFindViewById.html create mode 100644 idea/testData/android/lint/findViewById.kt create mode 100644 idea/testData/android/lintQuickfix/findViewById/nullableType.kt create mode 100644 idea/testData/android/lintQuickfix/findViewById/nullableType.kt.expected create mode 100644 idea/testData/android/lintQuickfix/findViewById/simple.kt create mode 100644 idea/testData/android/lintQuickfix/findViewById/simple.kt.expected diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt b/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt index 122e1ed2e8f..ed8ac919e10 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/ktPsiUtil.kt @@ -533,6 +533,15 @@ fun KtCallExpression.getOrCreateValueArgumentList(): KtValueArgumentList { typeArgumentList ?: calleeExpression) as KtValueArgumentList } +fun KtCallExpression.addTypeArgument(typeArgument: KtTypeProjection) { + if (typeArgumentList != null) { + typeArgumentList?.addArgument(typeArgument) + } + else { + addAfter(KtPsiFactory(this).createTypeArguments("<${typeArgument.text}>"), calleeExpression) + } +} + fun KtDeclaration.hasBody() = when (this) { is KtFunction -> hasBody() is KtProperty -> hasBody() diff --git a/download_android_sdk.xml b/download_android_sdk.xml index 08fd93a74ed..4d49c3ada49 100644 --- a/download_android_sdk.xml +++ b/download_android_sdk.xml @@ -34,7 +34,7 @@ - + diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/inspection/TypeParameterFindViewByIdInspection.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/inspection/TypeParameterFindViewByIdInspection.kt new file mode 100644 index 00000000000..19f058a472c --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/inspection/TypeParameterFindViewByIdInspection.kt @@ -0,0 +1,79 @@ +/* + * 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.inspection + +import com.intellij.codeInspection.* +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElementVisitor +import org.jetbrains.android.facet.AndroidFacet +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.KtPsiUtil.isUnsafeCast +import org.jetbrains.kotlin.psi.psiUtil.addTypeArgument +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall + +class TypeParameterFindViewByIdInspection : AbstractKotlinInspection(), CleanupLocalInspectionTool { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor { + val compileSdk = AndroidFacet.getInstance(session.file)?.androidModuleInfo?.buildSdkVersion?.apiLevel + if (compileSdk == null || compileSdk < 26) { + return KtVisitorVoid() + } + + return object : KtVisitorVoid() { + override fun visitCallExpression(expression: KtCallExpression) { + super.visitCallExpression(expression) + if (expression.calleeExpression?.text != "findViewById" || expression.typeArguments.isNotEmpty()) { + return + } + + val parentCast = (expression.parent as? KtBinaryExpressionWithTypeRHS)?.takeIf { isUnsafeCast(it) } ?: return + val typeText = parentCast.right?.getTypeTextWithoutQuestionMark() ?: return + val callableDescriptor = expression.getResolvedCall(expression.analyze())?.resultingDescriptor ?: return + if (callableDescriptor.name.asString() != "findViewById" || callableDescriptor.typeParameters.size != 1) { + return + } + + holder.registerProblem( + parentCast, + "Can be converted to findViewById<$typeText>(...)", + ConvertCastToFindViewByIdWithTypeParameter()) + } + } + } + + class ConvertCastToFindViewByIdWithTypeParameter : LocalQuickFix { + override fun getFamilyName(): String = "Convert cast to findViewById with type parameter" + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val cast = descriptor.psiElement as? KtBinaryExpressionWithTypeRHS ?: return + val typeText = cast.right?.getTypeTextWithoutQuestionMark() ?: return + val call = cast.left as? KtCallExpression ?: return + + val newCall = call.copy() as KtCallExpression + val typeArgument = KtPsiFactory(call).createTypeArgument(typeText) + newCall.addTypeArgument(typeArgument) + + cast.replace(newCall) + } + } + + companion object { + fun KtTypeReference.getTypeTextWithoutQuestionMark(): String? = + (typeElement as? KtNullableType)?.innerType?.text ?: typeElement?.text + } +} \ No newline at end of file diff --git a/idea/idea-android/tests/org/jetbrains/kotlin/android/TestUtils.java b/idea/idea-android/tests/org/jetbrains/kotlin/android/TestUtils.java index f422af3a5af..1d34458c91b 100644 --- a/idea/idea-android/tests/org/jetbrains/kotlin/android/TestUtils.java +++ b/idea/idea-android/tests/org/jetbrains/kotlin/android/TestUtils.java @@ -34,6 +34,6 @@ public class TestUtils { @NonNull public static String getLatestAndroidPlatform() { - return "android-25"; + return "android-26"; } } diff --git a/idea/idea-android/tests/org/jetbrains/kotlin/android/lint/AbstractKotlinLintTest.kt b/idea/idea-android/tests/org/jetbrains/kotlin/android/lint/AbstractKotlinLintTest.kt index 8efc853d4ca..33a62bf4159 100644 --- a/idea/idea-android/tests/org/jetbrains/kotlin/android/lint/AbstractKotlinLintTest.kt +++ b/idea/idea-android/tests/org/jetbrains/kotlin/android/lint/AbstractKotlinLintTest.kt @@ -16,6 +16,7 @@ package org.jetbrains.kotlin.android.lint +import com.intellij.codeInspection.InspectionProfileEntry import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl import com.intellij.util.PathUtil import org.jetbrains.android.inspections.klint.AndroidLintInspectionBase @@ -53,7 +54,7 @@ abstract class AbstractKotlinLintTest : KotlinAndroidTestCase() { myFixture.enableInspections(*inspectionClassNames.map { className -> val inspectionClass = Class.forName(className) - inspectionClass.newInstance() as AndroidLintInspectionBase + inspectionClass.newInstance() as InspectionProfileEntry }.toTypedArray()) val additionalResourcesDir = File(ktFile.parentFile, getTestName(true)) @@ -76,6 +77,6 @@ abstract class AbstractKotlinLintTest : KotlinAndroidTestCase() { myFixture.copyFileToProject("${PathUtil.getParentPath(path)}/$dependencyFile", "src/$dependencyTargetPath") } - myFixture.checkHighlighting(true, false, false) + myFixture.checkHighlighting(true, false, true) } } \ No newline at end of file diff --git a/idea/idea-android/tests/org/jetbrains/kotlin/android/lint/KotlinLintTestGenerated.java b/idea/idea-android/tests/org/jetbrains/kotlin/android/lint/KotlinLintTestGenerated.java index 657f086f8d6..b7832a46dec 100644 --- a/idea/idea-android/tests/org/jetbrains/kotlin/android/lint/KotlinLintTestGenerated.java +++ b/idea/idea-android/tests/org/jetbrains/kotlin/android/lint/KotlinLintTestGenerated.java @@ -66,6 +66,12 @@ public class KotlinLintTestGenerated extends AbstractKotlinLintTest { doTest(fileName); } + @TestMetadata("findViewById.kt") + public void testFindViewById() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/lint/findViewById.kt"); + doTest(fileName); + } + @TestMetadata("javaPerformance.kt") public void testJavaPerformance() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/lint/javaPerformance.kt"); diff --git a/idea/idea-android/tests/org/jetbrains/kotlin/android/quickfix/AndroidLintQuickfixTestGenerated.java b/idea/idea-android/tests/org/jetbrains/kotlin/android/quickfix/AndroidLintQuickfixTestGenerated.java index bc41d2eb35a..d2e45cd5b89 100644 --- a/idea/idea-android/tests/org/jetbrains/kotlin/android/quickfix/AndroidLintQuickfixTestGenerated.java +++ b/idea/idea-android/tests/org/jetbrains/kotlin/android/quickfix/AndroidLintQuickfixTestGenerated.java @@ -36,6 +36,27 @@ public class AndroidLintQuickfixTestGenerated extends AbstractAndroidLintQuickfi KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/android/lintQuickfix"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true); } + @TestMetadata("idea/testData/android/lintQuickfix/findViewById") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class FindViewById extends AbstractAndroidLintQuickfixTest { + public void testAllFilesPresentInFindViewById() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/android/lintQuickfix/findViewById"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("nullableType.kt") + public void testNullableType() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/lintQuickfix/findViewById/nullableType.kt"); + doTest(fileName); + } + + @TestMetadata("simple.kt") + public void testSimple() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/lintQuickfix/findViewById/simple.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/android/lintQuickfix/parcelable") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/idea/resources/inspectionDescriptions/TypeParameterFindViewById.html b/idea/resources/inspectionDescriptions/TypeParameterFindViewById.html new file mode 100644 index 00000000000..13c3dc2c2ed --- /dev/null +++ b/idea/resources/inspectionDescriptions/TypeParameterFindViewById.html @@ -0,0 +1,5 @@ + + +Reports findViewById calls with type cast which can be converted to findViewById with type parameter from Android 8.0 (API level 26) + + \ No newline at end of file diff --git a/idea/src/META-INF/android.xml b/idea/src/META-INF/android.xml index 3e04ed2fa49..b6a5e9931ee 100644 --- a/idea/src/META-INF/android.xml +++ b/idea/src/META-INF/android.xml @@ -12,10 +12,18 @@ + + diff --git a/idea/testData/android/lint/findViewById.kt b/idea/testData/android/lint/findViewById.kt new file mode 100644 index 00000000000..974ee5afd5a --- /dev/null +++ b/idea/testData/android/lint/findViewById.kt @@ -0,0 +1,49 @@ +// INSPECTION_CLASS: org.jetbrains.kotlin.android.inspection.TypeParameterFindViewByIdInspection + +import android.app.Activity +import android.os.Bundle +import android.widget.Button +import android.widget.TextView + +@Suppress( + "TYPE_INFERENCE_NO_INFORMATION_FOR_PARAMETER", + "UNUSED_VARIABLE" +) +class OtherActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_other) + + findViewById(R.id.tvHello) as TextView + val tvHello = findViewById(R.id.tvHello) as TextView + val btnGo = findViewById(R.id.btnGo) as Button? + + // should be ok, already has type parameter + val tvHello2 = findViewById(R.id.tvHello) as TextView + + // ok, we can't convert safe cast because semantic will be changed + val tvHello3 = findViewById(R.id.tvHello) as? TextView + + // ok, no cast + foo(findViewById(R.id.tvHello)) + + // ok, no cast + findViewById(R.id.tvHello) is TextView + } + + fun foo(view: TextView) { + view.text = "foo" + } +} + +class R { + object layout { + val activity_other = 100500 + } + + object id { + val tvHello = 0 + val btnGo = 1 + } +} \ No newline at end of file diff --git a/idea/testData/android/lint/viewHolder.kt b/idea/testData/android/lint/viewHolder.kt index ad2f7fc07e9..057b2d07aa8 100644 --- a/idea/testData/android/lint/viewHolder.kt +++ b/idea/testData/android/lint/viewHolder.kt @@ -29,7 +29,7 @@ abstract class ViewHolderTest : BaseAdapter() { // Should use View Holder pattern here convertView = mInflater.inflate(R.layout.your_layout, null) - val text = convertView.findViewById(R.id.text) as TextView + val text: TextView = convertView.findViewById(R.id.text) text.text = "Position " + position return convertView @@ -46,7 +46,7 @@ abstract class ViewHolderTest : BaseAdapter() { convertView = mInflater.inflate(R.layout.your_layout, null) } - val text = convertView!!.findViewById(R.id.text) as TextView + val text: TextView = convertView!!.findViewById(R.id.text) text.text = "Position " + position return convertView @@ -65,7 +65,7 @@ abstract class ViewHolderTest : BaseAdapter() { convertView = mInflater.inflate(R.layout.your_layout, null) } - val text = convertView!!.findViewById(R.id.text) as TextView + val text: TextView = convertView!!.findViewById(R.id.text) text.text = "Position " + position return convertView @@ -80,7 +80,7 @@ abstract class ViewHolderTest : BaseAdapter() { // Already using View Holder pattern convertView = if (convertView == null) mInflater.inflate(R.layout.your_layout, null) else convertView - val text = convertView!!.findViewById(R.id.text) as TextView + val text: TextView = convertView!!.findViewById(R.id.text) text.text = "Position " + position return convertView @@ -99,16 +99,16 @@ abstract class ViewHolderTest : BaseAdapter() { var v: View? = convertView if (v == null) v = mLayoutInflator!!.inflate(R.layout.your_layout, null) - val listItemHolder = v!!.findViewById(R.id.laptimes_list_item_holder) as LinearLayout + val listItemHolder: LinearLayout = v!!.findViewById(R.id.laptimes_list_item_holder) listItemHolder.removeAllViews() for (i in 1..5) { - val lapItemView = mLayoutInflator!!.inflate(R.layout.laptime_item, null) + val lapItemView: View = mLayoutInflator!!.inflate(R.layout.laptime_item, null) if (i == 0) { - val t = lapItemView.findViewById(R.id.laptime_text) as TextView + val t: TextView = lapItemView.findViewById(R.id.laptime_text) } - val t2 = lapItemView.findViewById(R.id.laptime_text2) as TextView + val t2: TextView = lapItemView.findViewById(R.id.laptime_text2) if (i < mLapTimes.size - 1 && mLapTimes.size > 1) { var laptime = mLapTimes[i] - mLapTimes[i + 1] if (laptime < 0) laptime = mLapTimes[i] diff --git a/idea/testData/android/lintQuickfix/findViewById/nullableType.kt b/idea/testData/android/lintQuickfix/findViewById/nullableType.kt new file mode 100644 index 00000000000..b0ae9f7508b --- /dev/null +++ b/idea/testData/android/lintQuickfix/findViewById/nullableType.kt @@ -0,0 +1,28 @@ +// INTENTION_TEXT: Convert cast to findViewById with type parameter +// INSPECTION_CLASS: org.jetbrains.kotlin.android.inspection.TypeParameterFindViewByIdInspection + +import android.app.Activity +import android.os.Bundle +import android.widget.Button +import android.widget.TextView + + +class OtherActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_other) + + val tvHello = findViewById(R.id.tvHello) as TextView? + } +} + +class R { + object layout { + val activity_other = 100500 + } + + object id { + val tvHello = 0 + } +} \ No newline at end of file diff --git a/idea/testData/android/lintQuickfix/findViewById/nullableType.kt.expected b/idea/testData/android/lintQuickfix/findViewById/nullableType.kt.expected new file mode 100644 index 00000000000..fbf229a7ee8 --- /dev/null +++ b/idea/testData/android/lintQuickfix/findViewById/nullableType.kt.expected @@ -0,0 +1,28 @@ +// INTENTION_TEXT: Convert cast to findViewById with type parameter +// INSPECTION_CLASS: org.jetbrains.kotlin.android.inspection.TypeParameterFindViewByIdInspection + +import android.app.Activity +import android.os.Bundle +import android.widget.Button +import android.widget.TextView + + +class OtherActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_other) + + val tvHello = findViewById(R.id.tvHello) + } +} + +class R { + object layout { + val activity_other = 100500 + } + + object id { + val tvHello = 0 + } +} \ No newline at end of file diff --git a/idea/testData/android/lintQuickfix/findViewById/simple.kt b/idea/testData/android/lintQuickfix/findViewById/simple.kt new file mode 100644 index 00000000000..788bc58c52c --- /dev/null +++ b/idea/testData/android/lintQuickfix/findViewById/simple.kt @@ -0,0 +1,28 @@ +// INTENTION_TEXT: Convert cast to findViewById with type parameter +// INSPECTION_CLASS: org.jetbrains.kotlin.android.inspection.TypeParameterFindViewByIdInspection + +import android.app.Activity +import android.os.Bundle +import android.widget.Button +import android.widget.TextView + + +class OtherActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_other) + + val tvHello = findViewById(R.id.tvHello) as TextView + } +} + +class R { + object layout { + val activity_other = 100500 + } + + object id { + val tvHello = 0 + } +} \ No newline at end of file diff --git a/idea/testData/android/lintQuickfix/findViewById/simple.kt.expected b/idea/testData/android/lintQuickfix/findViewById/simple.kt.expected new file mode 100644 index 00000000000..fbf229a7ee8 --- /dev/null +++ b/idea/testData/android/lintQuickfix/findViewById/simple.kt.expected @@ -0,0 +1,28 @@ +// INTENTION_TEXT: Convert cast to findViewById with type parameter +// INSPECTION_CLASS: org.jetbrains.kotlin.android.inspection.TypeParameterFindViewByIdInspection + +import android.app.Activity +import android.os.Bundle +import android.widget.Button +import android.widget.TextView + + +class OtherActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_other) + + val tvHello = findViewById(R.id.tvHello) + } +} + +class R { + object layout { + val activity_other = 100500 + } + + object id { + val tvHello = 0 + } +} \ No newline at end of file