Implemented @native-* annotation migration quickfix for extension functions

As the first part of KT-15270 Quickfix to migrate from @native***
This commit is contained in:
qx
2017-04-28 10:13:26 +03:00
parent a71e5abb45
commit c364e79557
10 changed files with 160 additions and 24 deletions
@@ -384,8 +384,8 @@ class QuickFixRegistrar : QuickFixContributor {
UNRESOLVED_REFERENCE.registerFactory(ReplaceObsoleteLabelSyntaxFix) UNRESOLVED_REFERENCE.registerFactory(ReplaceObsoleteLabelSyntaxFix)
DEPRECATION.registerFactory(DeprecatedSymbolUsageFix, DeprecatedSymbolUsageInWholeProjectFix) DEPRECATION.registerFactory(DeprecatedSymbolUsageFix, DeprecatedSymbolUsageInWholeProjectFix, MigrateExternalExtensionFix)
DEPRECATION_ERROR.registerFactory(DeprecatedSymbolUsageFix, DeprecatedSymbolUsageInWholeProjectFix) DEPRECATION_ERROR.registerFactory(DeprecatedSymbolUsageFix, DeprecatedSymbolUsageInWholeProjectFix, MigrateExternalExtensionFix)
PROTECTED_CALL_FROM_PUBLIC_INLINE.registerFactory(ReplaceProtectedToPublishedApiCallFix) PROTECTED_CALL_FROM_PUBLIC_INLINE.registerFactory(ReplaceProtectedToPublishedApiCallFix)
POSITIONED_VALUE_ARGUMENT_FOR_JAVA_ANNOTATION.registerFactory(ReplaceJavaAnnotationPositionedArgumentsFix) POSITIONED_VALUE_ARGUMENT_FOR_JAVA_ANNOTATION.registerFactory(ReplaceJavaAnnotationPositionedArgumentsFix)
@@ -19,9 +19,11 @@ package org.jetbrains.kotlin.idea.quickfix.migration
import com.intellij.codeInsight.intention.IntentionAction import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.annotations.checkAnnotationName import org.jetbrains.kotlin.descriptors.annotations.checkAnnotationName
import org.jetbrains.kotlin.diagnostics.Diagnostic import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.intentions.SpecifyTypeExplicitlyIntention import org.jetbrains.kotlin.idea.intentions.SpecifyTypeExplicitlyIntention
import org.jetbrains.kotlin.idea.project.builtIns import org.jetbrains.kotlin.idea.project.builtIns
@@ -30,6 +32,7 @@ import org.jetbrains.kotlin.idea.quickfix.KotlinQuickFixAction
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory
import org.jetbrains.kotlin.idea.util.addAnnotation import org.jetbrains.kotlin.idea.util.addAnnotation
import org.jetbrains.kotlin.js.PredefinedAnnotation import org.jetbrains.kotlin.js.PredefinedAnnotation
import org.jetbrains.kotlin.js.resolve.diagnostics.ErrorsJs
import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
@@ -44,13 +47,24 @@ class MigrateExternalExtensionFix(declaration: KtNamedDeclaration)
override fun invoke(project: Project, editor: Editor?, file: KtFile) { override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val declaration = element ?: return val declaration = element ?: return
if (isExternalMemberDeclaration(declaration)) {
fixExtensionMemberDeclaration(declaration, project, editor, file)
return
}
}
private fun fixExtensionMemberDeclaration(declaration: KtNamedDeclaration, project: Project, editor: Editor?, file: KtFile) {
val name = declaration.nameAsSafeName val name = declaration.nameAsSafeName
declaration.modifierList?.annotationEntries?.firstOrNull { it.isJsNative() }?.delete() val annotationEntries = declaration.modifierList?.annotationEntries
val isGetter = annotationEntries?.any { it.isJsAnnotation(PredefinedAnnotation.NATIVE_GETTER) } ?: false
val isSetter = annotationEntries?.any { it.isJsAnnotation(PredefinedAnnotation.NATIVE_SETTER) } ?: false
val isInvoke = annotationEntries?.any { it.isJsAnnotation(PredefinedAnnotation.NATIVE_INVOKE) } ?: false
annotationEntries?.filter { it.isJsNativeAnnotation() }?.forEach { it.delete() }
declaration.addModifier(KtTokens.INLINE_KEYWORD) declaration.addModifier(KtTokens.INLINE_KEYWORD)
declaration.removeModifier(KtTokens.EXTERNAL_KEYWORD) declaration.removeModifier(KtTokens.EXTERNAL_KEYWORD)
if (declaration is KtFunction) { if (declaration is KtFunction) {
declaration.addAnnotation(KotlinBuiltIns.FQ_NAMES.suppress.toSafe(), "\"NOTHING_TO_INLINE\"") declaration.addAnnotation(KotlinBuiltIns.FQ_NAMES.suppress.toSafe(), "\"NOTHING_TO_INLINE\"")
if (!declaration.hasDeclaredReturnType()) { if (!declaration.hasDeclaredReturnType() && !isSetter && !isInvoke) {
SpecifyTypeExplicitlyIntention.addTypeAnnotation(editor, declaration, declaration.builtIns.unitType) SpecifyTypeExplicitlyIntention.addTypeAnnotation(editor, declaration, declaration.builtIns.unitType)
} }
} }
@@ -58,16 +72,42 @@ class MigrateExternalExtensionFix(declaration: KtNamedDeclaration)
val ktPsiFactory = KtPsiFactory(project) val ktPsiFactory = KtPsiFactory(project)
val body = ktPsiFactory.buildExpression { val body = ktPsiFactory.buildExpression {
appendName(Name.identifier("asDynamic")) appendName(Name.identifier("asDynamic"))
appendFixedText("().") if (isGetter) {
appendName(name) appendFixedText("()")
if (declaration is KtNamedFunction) { if (declaration is KtNamedFunction) {
appendParameters(declaration) appendParameters(declaration, "[", "]")
}
} else if (isSetter) {
appendFixedText("()")
if (declaration is KtNamedFunction) {
appendParameters(declaration, "[", "]", skipLast = true)
declaration.valueParameters.last().nameAsName?.let {
appendFixedText(" = ")
appendName(it)
}
}
} else if (isInvoke) {
appendFixedText("()")
if (declaration is KtNamedFunction) {
appendParameters(declaration, "(", ")")
}
} else {
appendFixedText("().")
appendName(name)
if (declaration is KtNamedFunction) {
appendParameters(declaration, "(", ")")
}
} }
} }
if (declaration is KtNamedFunction) { if (declaration is KtNamedFunction) {
(declaration.bodyExpression as? KtBlockExpression)?.delete() declaration.bodyExpression?.delete()
declaration.bodyExpression?.replace(body) ?: run { declaration.equalsToken?.delete()
if (isSetter || isInvoke) {
val blockBody = ktPsiFactory.createSingleStatementBlock(body)
declaration.add(blockBody)
} else {
declaration.add(ktPsiFactory.createEQ()) declaration.add(ktPsiFactory.createEQ())
declaration.add(body) declaration.add(body)
} }
@@ -95,15 +135,9 @@ class MigrateExternalExtensionFix(declaration: KtNamedDeclaration)
} }
} }
private fun KtAnnotationEntry.isJsNative(): Boolean { private fun BuilderByPattern<KtExpression>.appendParameters(declaration: KtNamedFunction, lParenth: String, rParenth: String, skipLast: Boolean = false) {
val bindingContext = analyze(BodyResolveMode.PARTIAL_WITH_DIAGNOSTICS) appendFixedText(lParenth)
val annotationDescriptor = bindingContext[BindingContext.ANNOTATION, this] for ((index, param) in declaration.valueParameters.let { if (skipLast) it.take(it.size-1) else it }.withIndex()) {
return annotationDescriptor != null && checkAnnotationName(annotationDescriptor, PredefinedAnnotation.NATIVE.fqName)
}
private fun BuilderByPattern<KtExpression>.appendParameters(declaration: KtNamedFunction) {
appendFixedText("(")
for ((index, param) in declaration.valueParameters.withIndex()) {
param.nameAsName?.let { paramName -> param.nameAsName?.let { paramName ->
if (index > 0) { if (index > 0) {
appendFixedText(",") appendFixedText(",")
@@ -111,16 +145,55 @@ class MigrateExternalExtensionFix(declaration: KtNamedDeclaration)
appendName(paramName) appendName(paramName)
} }
} }
appendFixedText(")") appendFixedText(rParenth)
} }
companion object : KotlinSingleIntentionActionFactory() { companion object : KotlinSingleIntentionActionFactory() {
private fun KtAnnotationEntry.isJsAnnotation(vararg predefinedAnnotations: PredefinedAnnotation): Boolean {
val bindingContext = analyze(BodyResolveMode.PARTIAL_WITH_DIAGNOSTICS)
val annotationDescriptor = bindingContext[BindingContext.ANNOTATION, this]
return annotationDescriptor != null && predefinedAnnotations.any { checkAnnotationName(annotationDescriptor, it.fqName) }
}
private fun KtAnnotationEntry.isJsNativeAnnotation(): Boolean {
return isJsAnnotation(PredefinedAnnotation.NATIVE, PredefinedAnnotation.NATIVE_GETTER, PredefinedAnnotation.NATIVE_SETTER, PredefinedAnnotation.NATIVE_INVOKE )
}
private fun isExternalMemberDeclaration(psiElement: PsiElement): Boolean {
return (psiElement is KtNamedFunction && psiElement.receiverTypeReference != null) ||
(psiElement is KtProperty && psiElement.receiverTypeReference != null)
}
private inline fun<reified T: PsiElement> getContainingElement(e: PsiElement): T? {
var element: PsiElement? = e
while (element != null) {
if (element is T)
return element
element = element.parent
}
return null
}
override fun createAction(diagnostic: Diagnostic): IntentionAction? { override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val e = diagnostic.psiElement val e = diagnostic.psiElement
if ((e is KtNamedFunction && e.receiverTypeReference != null) || when (diagnostic.factory) {
(e is KtProperty && e.receiverTypeReference != null)) { ErrorsJs.WRONG_EXTERNAL_DECLARATION -> {
return MigrateExternalExtensionFix(e as KtNamedDeclaration) if (isExternalMemberDeclaration(e)) {
return MigrateExternalExtensionFix(e as KtNamedDeclaration)
}
}
Errors.DEPRECATION_ERROR, Errors.DEPRECATION -> {
if (getContainingElement<KtAnnotationEntry>(e)?.isJsNativeAnnotation() == true) {
getContainingElement<KtNamedDeclaration>(e)?.let {
return MigrateExternalExtensionFix(it)
}
}
if ((e as? KtNamedDeclaration)?.modifierList?.annotationEntries?.any { it.isJsNativeAnnotation() } == true) {
return MigrateExternalExtensionFix(e as KtNamedDeclaration)
}
}
} }
return null return null
} }
} }
@@ -0,0 +1,7 @@
// "Fix with 'asDynamic'" "true"
// JS
external class B
@<caret>nativeGetter
fun B.boo(i: Int): B?
@@ -0,0 +1,7 @@
// "Fix with 'asDynamic'" "true"
// JS
external class B
@Suppress("NOTHING_TO_INLINE")
inline fun B.boo(i: Int): B? = asDynamic()[i]
@@ -0,0 +1,7 @@
// "Fix with 'asDynamic'" "true"
// JS
external class B
@native<caret>Invoke
fun B.baz(a: B)
@@ -0,0 +1,9 @@
// "Fix with 'asDynamic'" "true"
// JS
external class B
@Suppress("NOTHING_TO_INLINE")
inline fun B.baz(a: B) {
asDynamic()(a)
}
@@ -0,0 +1,7 @@
// "Fix with 'asDynamic'" "true"
// JS
external class B
@<caret>nativeSetter
fun B.boo(i: Int, v: B)
@@ -0,0 +1,9 @@
// "Fix with 'asDynamic'" "true"
// JS
external class B
@Suppress("NOTHING_TO_INLINE")
inline fun B.boo(i: Int, v: B) {
asDynamic()[i] = v
}
@@ -6444,6 +6444,24 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/migration/jsExternal/nativeExtensionVaslJsRuntime.kt"); String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/migration/jsExternal/nativeExtensionVaslJsRuntime.kt");
doTest(fileName); doTest(fileName);
} }
@TestMetadata("nativeGetterExtensionFunJsRuntime.kt")
public void testNativeGetterExtensionFunJsRuntime() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/migration/jsExternal/nativeGetterExtensionFunJsRuntime.kt");
doTest(fileName);
}
@TestMetadata("nativeInvokeExtensionFunJsRuntime.kt")
public void testNativeInvokeExtensionFunJsRuntime() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/migration/jsExternal/nativeInvokeExtensionFunJsRuntime.kt");
doTest(fileName);
}
@TestMetadata("nativeSetterExtensionFunJsRuntime.kt")
public void testNativeSetterExtensionFunJsRuntime() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/migration/jsExternal/nativeSetterExtensionFunJsRuntime.kt");
doTest(fileName);
}
} }
@TestMetadata("idea/testData/quickfix/migration/missingConstructorKeyword") @TestMetadata("idea/testData/quickfix/migration/missingConstructorKeyword")
@@ -53,5 +53,4 @@ public class SourceMapGenerationSmokeTestGenerated extends AbstractSourceMapGene
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/sourcemap/methodCallInMethod.kt"); String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/sourcemap/methodCallInMethod.kt");
doTest(fileName); doTest(fileName);
} }
} }