diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/AddTargetApiQuickFix.kt.181 b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/AddTargetApiQuickFix.kt.181 new file mode 100644 index 00000000000..a05fc15de40 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/AddTargetApiQuickFix.kt.181 @@ -0,0 +1,95 @@ +/* + * 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.quickfix + +import com.android.SdkConstants +import com.android.tools.lint.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.inspections.lint.AndroidLintQuickFix +import org.jetbrains.android.inspections.lint.AndroidQuickfixContexts +import org.jetbrains.android.util.AndroidBundle +import org.jetbrains.kotlin.android.hasBackingField +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 = if (annotationContainer.isNewLineNeededForAnnotation()) "\n" else " ") + } + } + + private fun KtElement.isNewLineNeededForAnnotation() = !(this is KtParameter || this is KtTypeParameter || this is KtPropertyAccessor) + + private fun getAnnotationValue(fullyQualified: Boolean) = getVersionField(api, fullyQualified) + + private fun getAnnotationContainer(element: PsiElement, useRequiresApi: Boolean): PsiElement? { + return PsiTreeUtil.findFirstParent(element) { + if (useRequiresApi) + it.isRequiresApiAnnotationValidTarget() + else + it.isTargetApiAnnotationValidTarget() + } + } + + private fun PsiElement.isRequiresApiAnnotationValidTarget(): Boolean { + return this is KtClassOrObject || + (this is KtFunction && this !is KtFunctionLiteral) || + (this is KtProperty && !isLocal && hasBackingField()) || + this is KtPropertyAccessor + } + + private fun PsiElement.isTargetApiAnnotationValidTarget(): Boolean { + return this is KtClassOrObject || + (this is KtFunction && this !is KtFunctionLiteral) || + this is KtPropertyAccessor + } +} \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/AddTargetVersionCheckQuickFix.kt.181 b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/AddTargetVersionCheckQuickFix.kt.181 new file mode 100644 index 00000000000..d506bedc907 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/AddTargetVersionCheckQuickFix.kt.181 @@ -0,0 +1,94 @@ +/* + * 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.quickfix + +import com.intellij.codeInsight.FileModificationService +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.android.inspections.lint.AndroidLintQuickFix +import org.jetbrains.android.inspections.lint.AndroidQuickfixContexts +import org.jetbrains.kotlin.idea.caches.resolve.analyze +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.* +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode + + +class AddTargetVersionCheckQuickFix(val api: Int) : AndroidLintQuickFix { + + 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 surrounder = getSurrounder(targetExpression, "\"VERSION.SDK_INT < ${getVersionField(api, false)}\"") + val conditionRange = 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)}) { ... }" + + private fun getTargetExpression(element: PsiElement): KtElement? { + 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 || + current.parent is KtPropertyAccessor || + current.parent is KtProperty || + current.parent is KtReturnExpression || + current.parent is KtDestructuringDeclaration) { + break + } + current = PsiTreeUtil.getParentOfType(current, KtExpression::class.java, true) + } + + return current + } + + private fun getSurrounder(element: KtElement, todoText: String?): KotlinIfSurrounder { + val used = element.analyze(BodyResolveMode.PARTIAL)[BindingContext.USED_AS_EXPRESSION, element] ?: false + return if (used) { + object : KotlinIfSurrounder() { + override fun getCodeTemplate(): String = "if (a) { \n} else {\nTODO(${todoText ?: ""})\n}" + } + } else { + KotlinIfSurrounder() + } + } +} \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/ApiUtils.kt.181 b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/ApiUtils.kt.181 new file mode 100644 index 00000000000..3a52671ba84 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/ApiUtils.kt.181 @@ -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.kotlin.android.quickfix + +import com.android.sdklib.SdkVersionInfo.* + + +fun getVersionField(api: Int, fullyQualified: Boolean): String = getBuildCode(api)?.let { + if (fullyQualified) "android.os.Build.VERSION_CODES.$it" else it +} ?: api.toString() \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/KotlinAndroidQuickFixProvider.kt.181 b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/KotlinAndroidQuickFixProvider.kt.181 new file mode 100644 index 00000000000..68feed44bda --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/KotlinAndroidQuickFixProvider.kt.181 @@ -0,0 +1,80 @@ +/* + * 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.quickfix + +import com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX +import com.android.tools.lint.checks.ApiDetector +import com.android.tools.lint.checks.CommentDetector +import com.android.tools.lint.checks.ParcelDetector +import com.android.tools.lint.detector.api.Issue +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiElement +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.android.inspections.lint.AndroidLintQuickFix +import org.jetbrains.android.inspections.lint.AndroidLintQuickFixProvider +import java.util.regex.Pattern + + +class KotlinAndroidQuickFixProvider : AndroidLintQuickFixProvider { + override fun getQuickFixes( + issue: Issue, + startElement: PsiElement, + endElement: PsiElement, + message: String, + data: Any? + ): Array { + val fixes: Array = when (issue) { + ApiDetector.UNSUPPORTED, ApiDetector.INLINED -> getApiQuickFixes(issue, startElement, message) + ParcelDetector.ISSUE -> arrayOf(ParcelableQuickFix()) + else -> emptyArray() + } + + if (issue != CommentDetector.STOP_SHIP) { + return fixes + SuppressLintQuickFix(issue.id) + } + + return fixes + } + + fun getApiQuickFixes(issue: Issue, element: PsiElement, message: String): Array { + val api = getRequiredVersion(message) + if (api == -1) { + return AndroidLintQuickFix.EMPTY_ARRAY + } + + val project = element.project + if (JavaPsiFacade.getInstance(project).findClass(REQUIRES_API_ANNOTATION, GlobalSearchScope.allScope(project)) != null) { + return arrayOf(AddTargetApiQuickFix(api, true), AddTargetApiQuickFix(api, false), AddTargetVersionCheckQuickFix(api)) + } + + return arrayOf(AddTargetApiQuickFix(api, false), AddTargetVersionCheckQuickFix(api)) + } + + private fun getRequiredVersion(errorMessage: String): Int { + val pattern = Pattern.compile("\\s(\\d+)\\s") + val matcher = pattern.matcher(errorMessage) + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)) + } + + return -1 + } + + companion object { + val REQUIRES_API_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "RequiresApi" + } +} \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/ParcelableQuickFix.kt.181 b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/ParcelableQuickFix.kt.181 new file mode 100644 index 00000000000..d3442300f5e --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/ParcelableQuickFix.kt.181 @@ -0,0 +1,43 @@ +/* + * 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.quickfix + +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.android.inspections.lint.AndroidLintQuickFix +import org.jetbrains.android.inspections.lint.AndroidQuickfixContexts +import org.jetbrains.android.util.AndroidBundle +import org.jetbrains.kotlin.android.canAddParcelable +import org.jetbrains.kotlin.android.implementParcelable +import org.jetbrains.kotlin.android.isParcelize +import org.jetbrains.kotlin.psi.KtClass + + +class ParcelableQuickFix : AndroidLintQuickFix { + override fun apply(startElement: PsiElement, endElement: PsiElement, context: AndroidQuickfixContexts.Context) { + startElement.getTargetClass()?.implementParcelable() + } + + override fun isApplicable(startElement: PsiElement, endElement: PsiElement, contextType: AndroidQuickfixContexts.ContextType): Boolean { + val targetClass = startElement.getTargetClass() ?: return false + return targetClass.canAddParcelable() && !targetClass.isParcelize() + } + + override fun getName(): String = AndroidBundle.message("implement.parcelable.intention.text") + + private fun PsiElement.getTargetClass(): KtClass? = PsiTreeUtil.getParentOfType(this, KtClass::class.java, false) +} \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/SuppressLintQuickFix.kt.181 b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/SuppressLintQuickFix.kt.181 new file mode 100644 index 00000000000..724f718c257 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/SuppressLintQuickFix.kt.181 @@ -0,0 +1,99 @@ +/* + * 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.quickfix + +import com.android.SdkConstants +import com.intellij.codeInsight.FileModificationService +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.android.inspections.lint.AndroidLintQuickFix +import org.jetbrains.android.inspections.lint.AndroidQuickfixContexts +import org.jetbrains.android.util.AndroidBundle +import org.jetbrains.kotlin.android.hasBackingField +import org.jetbrains.kotlin.idea.util.addAnnotation +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.* + + +class SuppressLintQuickFix(id: String) : AndroidLintQuickFix { + private val lintId = getLintId(id) + + override fun apply(startElement: PsiElement, endElement: PsiElement, context: AndroidQuickfixContexts.Context) { + val annotationContainer = PsiTreeUtil.findFirstParent(startElement, true) { it.isSuppressLintTarget() } ?: return + if (!FileModificationService.getInstance().preparePsiElementForWrite(annotationContainer)) { + return + } + + val argument = "\"$lintId\"" + + when (annotationContainer) { + is KtModifierListOwner -> { + annotationContainer.addAnnotation( + FQNAME_SUPPRESS_LINT, + argument, + whiteSpaceText = if (annotationContainer.isNewLineNeededForAnnotation()) "\n" else " ", + addToExistingAnnotation = { entry -> addArgumentToAnnotation(entry, argument) }) + } + } + } + + override fun getName(): String = AndroidBundle.message(SUPPRESS_LINT_MESSAGE, lintId) + + override fun isApplicable( + startElement: PsiElement, + endElement: PsiElement, + contextType: AndroidQuickfixContexts.ContextType + ): Boolean = true + + private fun addArgumentToAnnotation(entry: KtAnnotationEntry, argument: String): Boolean { + // add new arguments to an existing entry + val args = entry.valueArgumentList + val psiFactory = KtPsiFactory(entry) + val newArgList = psiFactory.createCallArguments("($argument)") + when { + args == null -> // new argument list + entry.addAfter(newArgList, entry.lastChild) + args.arguments.isEmpty() -> // replace '()' with a new argument list + args.replace(newArgList) + args.arguments.none { it.textMatches(argument) } -> + args.addArgument(newArgList.arguments[0]) + } + + return true + } + + private fun getLintId(intentionId: String) = + if (intentionId.startsWith(INTENTION_NAME_PREFIX)) intentionId.substring(INTENTION_NAME_PREFIX.length) else intentionId + + private fun KtElement.isNewLineNeededForAnnotation(): Boolean { + return !(this is KtParameter || + this is KtTypeParameter || + this is KtPropertyAccessor) + } + + private fun PsiElement.isSuppressLintTarget(): Boolean { + return this is KtDeclaration && + (this as? KtProperty)?.hasBackingField() ?: true && + this !is KtFunctionLiteral && + this !is KtDestructuringDeclaration + } + private companion object { + val INTENTION_NAME_PREFIX = "AndroidLint" + val SUPPRESS_LINT_MESSAGE = "android.lint.fix.suppress.lint.api.annotation" + val FQNAME_SUPPRESS_LINT = FqName(SdkConstants.FQCN_SUPPRESS_LINT) + } +} \ No newline at end of file diff --git a/idea/src/META-INF/android.xml.181 b/idea/src/META-INF/android.xml.181 new file mode 100644 index 00000000000..4cf452c0bff --- /dev/null +++ b/idea/src/META-INF/android.xml.181 @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + org.jetbrains.kotlin.android.intention.KotlinAndroidAddStringResource + Kotlin Android + + + + org.jetbrains.kotlin.android.intention.AddActivityToManifest + Kotlin Android + + + + org.jetbrains.kotlin.android.intention.AddServiceToManifest + Kotlin Android + + + + org.jetbrains.kotlin.android.intention.AddBroadcastReceiverToManifest + Kotlin Android + + + org.jetbrains.kotlin.android.intention.ImplementParcelableAction + Kotlin Android + + + + org.jetbrains.kotlin.android.intention.RemoveParcelableAction + Kotlin Android + + + + org.jetbrains.kotlin.android.intention.RedoParcelableAction + Kotlin Android + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +