Implement quickfixes for Android Lint api issues
#KT-16624 Fixed #KT-16625 Fixed #KT-14947 Fixed
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
+86
@@ -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)
|
||||
}
|
||||
+78
@@ -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
|
||||
}
|
||||
}
|
||||
+83
@@ -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()
|
||||
Reference in New Issue
Block a user