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)
DEPRECATION.registerFactory(DeprecatedSymbolUsageFix, DeprecatedSymbolUsageInWholeProjectFix)
DEPRECATION_ERROR.registerFactory(DeprecatedSymbolUsageFix, DeprecatedSymbolUsageInWholeProjectFix)
DEPRECATION.registerFactory(DeprecatedSymbolUsageFix, DeprecatedSymbolUsageInWholeProjectFix, MigrateExternalExtensionFix)
DEPRECATION_ERROR.registerFactory(DeprecatedSymbolUsageFix, DeprecatedSymbolUsageInWholeProjectFix, MigrateExternalExtensionFix)
PROTECTED_CALL_FROM_PUBLIC_INLINE.registerFactory(ReplaceProtectedToPublishedApiCallFix)
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.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.annotations.checkAnnotationName
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.intentions.SpecifyTypeExplicitlyIntention
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.util.addAnnotation
import org.jetbrains.kotlin.js.PredefinedAnnotation
import org.jetbrains.kotlin.js.resolve.diagnostics.ErrorsJs
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
@@ -44,13 +47,24 @@ class MigrateExternalExtensionFix(declaration: KtNamedDeclaration)
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
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
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.removeModifier(KtTokens.EXTERNAL_KEYWORD)
if (declaration is KtFunction) {
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)
}
}
@@ -58,16 +72,42 @@ class MigrateExternalExtensionFix(declaration: KtNamedDeclaration)
val ktPsiFactory = KtPsiFactory(project)
val body = ktPsiFactory.buildExpression {
appendName(Name.identifier("asDynamic"))
appendFixedText("().")
appendName(name)
if (declaration is KtNamedFunction) {
appendParameters(declaration)
if (isGetter) {
appendFixedText("()")
if (declaration is KtNamedFunction) {
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) {
(declaration.bodyExpression as? KtBlockExpression)?.delete()
declaration.bodyExpression?.replace(body) ?: run {
declaration.bodyExpression?.delete()
declaration.equalsToken?.delete()
if (isSetter || isInvoke) {
val blockBody = ktPsiFactory.createSingleStatementBlock(body)
declaration.add(blockBody)
} else {
declaration.add(ktPsiFactory.createEQ())
declaration.add(body)
}
@@ -95,15 +135,9 @@ class MigrateExternalExtensionFix(declaration: KtNamedDeclaration)
}
}
private fun KtAnnotationEntry.isJsNative(): Boolean {
val bindingContext = analyze(BodyResolveMode.PARTIAL_WITH_DIAGNOSTICS)
val annotationDescriptor = bindingContext[BindingContext.ANNOTATION, this]
return annotationDescriptor != null && checkAnnotationName(annotationDescriptor, PredefinedAnnotation.NATIVE.fqName)
}
private fun BuilderByPattern<KtExpression>.appendParameters(declaration: KtNamedFunction) {
appendFixedText("(")
for ((index, param) in declaration.valueParameters.withIndex()) {
private fun BuilderByPattern<KtExpression>.appendParameters(declaration: KtNamedFunction, lParenth: String, rParenth: String, skipLast: Boolean = false) {
appendFixedText(lParenth)
for ((index, param) in declaration.valueParameters.let { if (skipLast) it.take(it.size-1) else it }.withIndex()) {
param.nameAsName?.let { paramName ->
if (index > 0) {
appendFixedText(",")
@@ -111,16 +145,55 @@ class MigrateExternalExtensionFix(declaration: KtNamedDeclaration)
appendName(paramName)
}
}
appendFixedText(")")
appendFixedText(rParenth)
}
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? {
val e = diagnostic.psiElement
if ((e is KtNamedFunction && e.receiverTypeReference != null) ||
(e is KtProperty && e.receiverTypeReference != null)) {
return MigrateExternalExtensionFix(e as KtNamedDeclaration)
when (diagnostic.factory) {
ErrorsJs.WRONG_EXTERNAL_DECLARATION -> {
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
}
}
@@ -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");
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")
@@ -53,5 +53,4 @@ public class SourceMapGenerationSmokeTestGenerated extends AbstractSourceMapGene
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/sourcemap/methodCallInMethod.kt");
doTest(fileName);
}
}