Implement quickfixes for Android Lint api issues

#KT-16624 Fixed
#KT-16625 Fixed
#KT-14947 Fixed
This commit is contained in:
Vyacheslav Gerasimov
2017-03-03 16:12:54 +03:00
parent a907ec92b5
commit 1376c8f8cf
178 changed files with 1307 additions and 221 deletions
@@ -50,6 +50,8 @@ import org.w3c.dom.NodeList;
import java.io.File;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.android.SdkConstants.*;
import static com.android.tools.klint.detector.api.ClassContext.getFqcn;
@@ -100,9 +102,9 @@ public class ApiDetector extends ResourceXmlDetector
Severity.ERROR,
new Implementation(
ApiDetector.class,
EnumSet.of(Scope.CLASS_FILE, Scope.RESOURCE_FILE, Scope.MANIFEST),
EnumSet.of(Scope.JAVA_FILE, Scope.RESOURCE_FILE, Scope.MANIFEST),
Scope.JAVA_FILE_SCOPE,
Scope.RESOURCE_FILE_SCOPE,
Scope.CLASS_FILE_SCOPE,
Scope.MANIFEST_SCOPE));
/** Accessing an inlined API on older platforms */
@@ -873,6 +875,16 @@ public class ApiDetector extends ResourceXmlDetector
return false;
}
public static int getRequiredVersion(String errorMessage) {
Pattern pattern = Pattern.compile("\\s(\\d+)\\s");
Matcher matcher = pattern.matcher(errorMessage);
if (matcher.find()) {
return Integer.parseInt(matcher.group(1));
}
return -1;
}
private final class ApiVisitor extends AbstractUastVisitor {
private final JavaContext mContext;
+1
View File
@@ -18,5 +18,6 @@
<orderEntry type="module" module-name="frontend" />
<orderEntry type="module" module-name="idea-analysis" />
<orderEntry type="module" module-name="idea-core" />
<orderEntry type="module" module-name="idea" />
</component>
</module>
@@ -0,0 +1,86 @@
/*
* 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.android.inspections.klint
import com.android.SdkConstants
import com.android.tools.klint.checks.ApiDetector.REQUIRES_API_ANNOTATION
import com.intellij.codeInsight.FileModificationService
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.android.util.AndroidBundle
import org.jetbrains.kotlin.idea.util.addAnnotation
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.*
class AddTargetApiQuickFix(
val api: Int,
val useRequiresApi: Boolean
) : AndroidLintQuickFix {
private companion object {
val FQNAME_TARGET_API = FqName(SdkConstants.FQCN_TARGET_API)
val FQNAME_REQUIRES_API = FqName(REQUIRES_API_ANNOTATION)
}
override fun isApplicable(startElement: PsiElement, endElement: PsiElement, contextType: AndroidQuickfixContexts.ContextType): Boolean =
getAnnotationContainer(startElement, useRequiresApi) != null
override fun getName(): String = getAnnotationValue(false).let {
if (useRequiresApi) {
// Not Available in Android plugin 2.0
// AndroidBundle.message("android.lint.fix.add.requires.api", it)
"Add @RequiresApi($it) Annotation"
} else {
AndroidBundle.message("android.lint.fix.add.target.api", it)
}
}
override fun apply(startElement: PsiElement, endElement: PsiElement, context: AndroidQuickfixContexts.Context) {
val annotationContainer = getAnnotationContainer(startElement, useRequiresApi) ?: return
if (!FileModificationService.getInstance().preparePsiElementForWrite(annotationContainer)) {
return
}
if (annotationContainer is KtModifierListOwner) {
annotationContainer.addAnnotation(
if (useRequiresApi) FQNAME_REQUIRES_API else FQNAME_TARGET_API,
getAnnotationValue(true),
whiteSpaceText = "\n")
}
}
private fun getAnnotationValue(fullyQualified: Boolean) = getVersionField(api, fullyQualified)
private fun getAnnotationContainer(element: PsiElement, useRequiresApi: Boolean) =
PsiTreeUtil.findFirstParent(element) {
if (useRequiresApi)
it.isRequiresApiAnnotationValidTarget()
else
it.isTargetApiAnnotationValidTarget()
}
// TODO: KtFunctionLiteral is not supported now because addAnnotation fails to shorten references, investigate
private fun PsiElement.isRequiresApiAnnotationValidTarget() = this is KtClassOrObject ||
(this is KtFunction && this !is KtFunctionLiteral) ||
(this is KtProperty && !this.isLocal)
// TODO: KtFunctionLiteral is not supported now because addAnnotation fails to shorten references, investigate
private fun PsiElement.isTargetApiAnnotationValidTarget() = this is KtClassOrObject ||
(this is KtFunction && this !is KtFunctionLiteral)
}
@@ -0,0 +1,78 @@
/*
* 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.android.inspections.klint
import com.intellij.codeInsight.FileModificationService
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.idea.codeInsight.surroundWith.statement.KotlinIfSurrounder
import org.jetbrains.kotlin.idea.core.ShortenReferences
import org.jetbrains.kotlin.idea.inspections.findExistingEditor
import org.jetbrains.kotlin.psi.*
class AddTargetVersionCheckQuickFix(val api: Int) : AndroidLintQuickFix {
companion object {
private val IF_SURROUNDER = KotlinIfSurrounder()
}
override fun apply(startElement: PsiElement, endElement: PsiElement, context: AndroidQuickfixContexts.Context) {
val targetExpression = getTargetExpression(startElement)
val project = targetExpression?.project ?: return
val editor = targetExpression.findExistingEditor() ?: return
val file = targetExpression.containingFile
val documentManager = PsiDocumentManager.getInstance(project)
val document = documentManager.getDocument(file) ?: return
if (!FileModificationService.getInstance().prepareFileForWrite(file)) {
return
}
val conditionRange = IF_SURROUNDER.surroundElements(project, editor, arrayOf(targetExpression)) ?: return
val conditionText = "android.os.Build.VERSION.SDK_INT >= ${getVersionField(api, true)}"
document.replaceString(conditionRange.startOffset, conditionRange.endOffset, conditionText)
documentManager.commitDocument(document)
ShortenReferences.DEFAULT.process(documentManager.getPsiFile(document) as KtFile,
conditionRange.startOffset,
conditionRange.startOffset + conditionText.length)
}
override fun isApplicable(startElement: PsiElement, endElement: PsiElement, contextType: AndroidQuickfixContexts.ContextType): Boolean =
getTargetExpression(startElement) != null
override fun getName(): String = "Surround with if (VERSION.SDK_INT >= VERSION_CODES.${getVersionField(api, false)}) { ... }"
// TODO: Delegated property, initializer, annotation parameter
private fun getTargetExpression(element: PsiElement): PsiElement? {
var current = PsiTreeUtil.getParentOfType(element, KtExpression::class.java)
while (current != null) {
if (current.parent is KtBlockExpression ||
current.parent is KtContainerNode ||
current.parent is KtWhenEntry ||
current.parent is KtFunction) {
break
}
current = PsiTreeUtil.getParentOfType(current, KtExpression::class.java, true)
}
return current
}
}
@@ -12,7 +12,68 @@ import com.android.sdklib.SdkVersionInfo;
import com.android.tools.idea.actions.OverrideResourceAction;
import com.android.tools.idea.templates.RepositoryUrlManager;
import com.android.tools.klint.checks.*;
import com.android.tools.klint.checks.AddJavascriptInterfaceDetector;
import com.android.tools.klint.checks.AlarmDetector;
import com.android.tools.klint.checks.AllowAllHostnameVerifierDetector;
import com.android.tools.klint.checks.AlwaysShowActionDetector;
import com.android.tools.klint.checks.AndroidAutoDetector;
import com.android.tools.klint.checks.AnnotationDetector;
import com.android.tools.klint.checks.ApiDetector;
import com.android.tools.klint.checks.AppCompatCallDetector;
import com.android.tools.klint.checks.AppIndexingApiDetector;
import com.android.tools.klint.checks.BadHostnameVerifierDetector;
import com.android.tools.klint.checks.CallSuperDetector;
import com.android.tools.klint.checks.CipherGetInstanceDetector;
import com.android.tools.klint.checks.CleanupDetector;
import com.android.tools.klint.checks.CommentDetector;
import com.android.tools.klint.checks.CustomViewDetector;
import com.android.tools.klint.checks.CutPasteDetector;
import com.android.tools.klint.checks.DateFormatDetector;
import com.android.tools.klint.checks.FragmentDetector;
import com.android.tools.klint.checks.GetSignaturesDetector;
import com.android.tools.klint.checks.HandlerDetector;
import com.android.tools.klint.checks.IconDetector;
import com.android.tools.klint.checks.JavaPerformanceDetector;
import com.android.tools.klint.checks.JavaScriptInterfaceDetector;
import com.android.tools.klint.checks.LayoutConsistencyDetector;
import com.android.tools.klint.checks.LayoutInflationDetector;
import com.android.tools.klint.checks.LocaleDetector;
import com.android.tools.klint.checks.LogDetector;
import com.android.tools.klint.checks.MathDetector;
import com.android.tools.klint.checks.MergeRootFrameLayoutDetector;
import com.android.tools.klint.checks.NonInternationalizedSmsDetector;
import com.android.tools.klint.checks.OverdrawDetector;
import com.android.tools.klint.checks.OverrideConcreteDetector;
import com.android.tools.klint.checks.ParcelDetector;
import com.android.tools.klint.checks.PreferenceActivityDetector;
import com.android.tools.klint.checks.PrivateResourceDetector;
import com.android.tools.klint.checks.ReadParcelableDetector;
import com.android.tools.klint.checks.RecyclerViewDetector;
import com.android.tools.klint.checks.RegistrationDetector;
import com.android.tools.klint.checks.RequiredAttributeDetector;
import com.android.tools.klint.checks.RtlDetector;
import com.android.tools.klint.checks.SQLiteDetector;
import com.android.tools.klint.checks.SdCardDetector;
import com.android.tools.klint.checks.SecureRandomDetector;
import com.android.tools.klint.checks.SecurityDetector;
import com.android.tools.klint.checks.ServiceCastDetector;
import com.android.tools.klint.checks.SetJavaScriptEnabledDetector;
import com.android.tools.klint.checks.SetTextDetector;
import com.android.tools.klint.checks.SslCertificateSocketFactoryDetector;
import com.android.tools.klint.checks.StringFormatDetector;
import com.android.tools.klint.checks.ToastDetector;
import com.android.tools.klint.checks.TrustAllX509TrustManagerDetector;
import com.android.tools.klint.checks.UnsafeBroadcastReceiverDetector;
import com.android.tools.klint.checks.UnsafeNativeCodeDetector;
import com.android.tools.klint.checks.ViewConstructorDetector;
import com.android.tools.klint.checks.ViewHolderDetector;
import com.android.tools.klint.checks.ViewTagDetector;
import com.android.tools.klint.checks.ViewTypeDetector;
import com.android.tools.klint.checks.WrongCallDetector;
import com.android.tools.klint.checks.WrongImportDetector;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.lint.checks.*;
import com.android.tools.lint.detector.api.TextFormat;
import com.google.common.collect.Lists;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.openapi.application.ApplicationManager;
@@ -410,6 +471,28 @@ public class AndroidLintInspectionToolProvider {
super(AndroidBundle.message("android.lint.inspections.new.api"), ApiDetector.UNSUPPORTED);
}
@NotNull
@Override
public AndroidLintQuickFix[] getQuickFixes(@NotNull PsiElement startElement, @NotNull PsiElement endElement, @NotNull String message) {
int api = ApiDetector.getRequiredVersion(TextFormat.RAW.toText(message));
if (api == -1) {
return AndroidLintQuickFix.EMPTY_ARRAY;
}
Project project = startElement.getProject();
if (JavaPsiFacade.getInstance(project).findClass(REQUIRES_API_ANNOTATION, GlobalSearchScope.allScope(project)) != null) {
return new AndroidLintQuickFix[] {
new AddTargetApiQuickFix(api, true),
new AddTargetApiQuickFix(api, false),
new AddTargetVersionCheckQuickFix(api)
};
}
return new AndroidLintQuickFix[] {
new AddTargetApiQuickFix(api, false),
new AddTargetVersionCheckQuickFix(api)
};
}
}
public static class AndroidKLintInlinedApiInspection extends AndroidLintInspectionBase {
@@ -0,0 +1,24 @@
/*
* 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.android.inspections.klint
import com.android.sdklib.SdkVersionInfo
fun getVersionField(api: Int, fullyQualified: Boolean): String = SdkVersionInfo.getBuildCode(api)?.let {
if (fullyQualified) "android.os.Build.VERSION_CODES.$it" else it
} ?: api.toString()