Add 'Incorrect Transient' diagnostic

when users write @Transient, they get @kotlin.jvm.Transient, because it's always in auto-import.
This annotation does not work with @Serializable classes; @kotlinx.serialization.Transient should be used instead.

Add quickfix for 'incorrect transient' diagnostic:
'Import kotlinx.serialization.Transient'
This commit is contained in:
Leonid Startsev
2020-07-03 18:30:07 +03:00
parent 6ef597a4ee
commit 340deb01dd
11 changed files with 138 additions and 5 deletions
@@ -5,5 +5,6 @@
<jsSyntheticTranslateExtension implementation="org.jetbrains.kotlinx.serialization.compiler.extensions.SerializationIDEJsExtension"/> <jsSyntheticTranslateExtension implementation="org.jetbrains.kotlinx.serialization.compiler.extensions.SerializationIDEJsExtension"/>
<irGenerationExtension implementation="org.jetbrains.kotlinx.serialization.compiler.extensions.SerializationIDEIrExtension"/> <irGenerationExtension implementation="org.jetbrains.kotlinx.serialization.compiler.extensions.SerializationIDEIrExtension"/>
<storageComponentContainerContributor implementation="org.jetbrains.kotlinx.serialization.compiler.extensions.SerializationIDEContainerContributor"/> <storageComponentContainerContributor implementation="org.jetbrains.kotlinx.serialization.compiler.extensions.SerializationIDEContainerContributor"/>
<quickFixContributor implementation="org.jetbrains.kotlinx.serialization.idea.quickfixes.SerializationQuickFixContributor"/>
</extensions> </extensions>
</idea-plugin> </idea-plugin>
@@ -1,5 +1,5 @@
/* /*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@@ -32,6 +32,8 @@ public interface SerializationErrors {
DiagnosticFactory0<PsiElement> TRANSIENT_IS_REDUNDANT = DiagnosticFactory0.create(WARNING); DiagnosticFactory0<PsiElement> TRANSIENT_IS_REDUNDANT = DiagnosticFactory0.create(WARNING);
DiagnosticFactory0<PsiElement> INCORRECT_TRANSIENT = DiagnosticFactory0.create(WARNING);
@SuppressWarnings("UnusedDeclaration") @SuppressWarnings("UnusedDeclaration")
Object _initializer = new Object() { Object _initializer = new Object() {
{ {
@@ -1,5 +1,5 @@
/* /*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@@ -14,10 +14,14 @@ import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingTrace import org.jetbrains.kotlin.resolve.BindingTrace
import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker
import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassOrAny import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassOrAny
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperInterfaces
import org.jetbrains.kotlin.resolve.hasBackingField import org.jetbrains.kotlin.resolve.hasBackingField
import org.jetbrains.kotlin.resolve.isInlineClassType import org.jetbrains.kotlin.resolve.isInlineClassType
import org.jetbrains.kotlin.resolve.jvm.annotations.TRANSIENT_ANNOTATION_FQ_NAME
import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyAnnotationDescriptor import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyAnnotationDescriptor
import org.jetbrains.kotlin.resolve.source.getPsi
import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.typeUtil.supertypes import org.jetbrains.kotlin.types.typeUtil.supertypes
import org.jetbrains.kotlin.util.slicedMap.Slices import org.jetbrains.kotlin.util.slicedMap.Slices
@@ -37,10 +41,27 @@ open class SerializationPluginDeclarationChecker : DeclarationChecker {
if (!canBeSerializedInternally(descriptor, declaration, context.trace)) return if (!canBeSerializedInternally(descriptor, declaration, context.trace)) return
if (declaration !is KtPureClassOrObject) return if (declaration !is KtPureClassOrObject) return
val props = buildSerializableProperties(descriptor, context.trace) ?: return val props = buildSerializableProperties(descriptor, context.trace) ?: return
checkCorrectTransientAnnotationIsUsed(descriptor, props.serializableProperties, context.trace)
checkTransients(declaration, context.trace) checkTransients(declaration, context.trace)
analyzePropertiesSerializers(context.trace, descriptor, props.serializableProperties) analyzePropertiesSerializers(context.trace, descriptor, props.serializableProperties)
} }
private fun checkCorrectTransientAnnotationIsUsed(
descriptor: ClassDescriptor,
properties: List<SerializableProperty>,
trace: BindingTrace
) {
if (descriptor.getSuperInterfaces().any { it.fqNameSafe.asString() == "java.io.Serializable" }) return // do not check
for (prop in properties) {
if (prop.transient) continue // correct annotation is used
val incorrectTransient = prop.descriptor.backingField?.annotations?.findAnnotation(TRANSIENT_ANNOTATION_FQ_NAME)
if (incorrectTransient != null) {
val elementToReport = incorrectTransient.source.getPsi() ?: prop.descriptor.findPsi() ?: continue
trace.report(SerializationErrors.INCORRECT_TRANSIENT.on(elementToReport))
}
}
}
private fun canBeSerializedInternally(descriptor: ClassDescriptor, declaration: KtDeclaration, trace: BindingTrace): Boolean { private fun canBeSerializedInternally(descriptor: ClassDescriptor, declaration: KtDeclaration, trace: BindingTrace): Boolean {
if (descriptor.isSerializableEnumWithMissingSerializer()) { if (descriptor.isSerializableEnumWithMissingSerializer()) {
val declarationToReport = declaration.modifierList ?: declaration val declarationToReport = declaration.modifierList ?: declaration
@@ -65,5 +65,9 @@ object SerializationPluginErrorsRendering : DefaultErrorMessages.Extension {
SerializationErrors.TRANSIENT_IS_REDUNDANT, SerializationErrors.TRANSIENT_IS_REDUNDANT,
"Property does not have backing field which makes it non-serializable and therefore @Transient is redundant" "Property does not have backing field which makes it non-serializable and therefore @Transient is redundant"
) )
MAP.put(
SerializationErrors.INCORRECT_TRANSIENT,
"@kotlin.jvm.Transient does not affect @Serializable classes. Please use @kotlinx.serialization.Transient instead."
)
} }
} }
@@ -23,7 +23,7 @@ object SerializationAnnotations {
val serializerAnnotationFqName = FqName("kotlinx.serialization.Serializer") val serializerAnnotationFqName = FqName("kotlinx.serialization.Serializer")
internal val serialNameAnnotationFqName = FqName("kotlinx.serialization.SerialName") internal val serialNameAnnotationFqName = FqName("kotlinx.serialization.SerialName")
internal val requiredAnnotationFqName = FqName("kotlinx.serialization.Required") internal val requiredAnnotationFqName = FqName("kotlinx.serialization.Required")
internal val serialTransientFqName = FqName("kotlinx.serialization.Transient") val serialTransientFqName = FqName("kotlinx.serialization.Transient")
internal val serialInfoFqName = FqName("kotlinx.serialization.SerialInfo") internal val serialInfoFqName = FqName("kotlinx.serialization.SerialInfo")
internal val contextualFqName = FqName("kotlinx.serialization.ContextualSerialization") // this one is deprecated internal val contextualFqName = FqName("kotlinx.serialization.ContextualSerialization") // this one is deprecated
@@ -25,7 +25,9 @@ public class SerializationPluginDiagnosticTestGenerated extends AbstractSerializ
} }
public void testAllFilesPresentInDiagnostics() throws Exception { public void testAllFilesPresentInDiagnostics() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/kotlin-serialization/kotlin-serialization-compiler/testData/diagnostics"), Pattern.compile("^(.+)\\.kt$"), null, true); KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File(
"plugins/kotlin-serialization/kotlin-serialization-compiler/testData/diagnostics"), Pattern.compile("^(.+)\\.kt$"), null,
true);
} }
@TestMetadata("DuplicateSerialName.kt") @TestMetadata("DuplicateSerialName.kt")
@@ -33,6 +35,16 @@ public class SerializationPluginDiagnosticTestGenerated extends AbstractSerializ
runTest("plugins/kotlin-serialization/kotlin-serialization-compiler/testData/diagnostics/DuplicateSerialName.kt"); runTest("plugins/kotlin-serialization/kotlin-serialization-compiler/testData/diagnostics/DuplicateSerialName.kt");
} }
@TestMetadata("IncorrectTransient.kt")
public void testIncorrectTransient() throws Exception {
runTest("plugins/kotlin-serialization/kotlin-serialization-compiler/testData/diagnostics/IncorrectTransient.kt");
}
@TestMetadata("IncorrectTransient2.kt")
public void testIncorrectTransient2() throws Exception {
runTest("plugins/kotlin-serialization/kotlin-serialization-compiler/testData/diagnostics/IncorrectTransient2.kt");
}
@TestMetadata("LazyRecursionBug.kt") @TestMetadata("LazyRecursionBug.kt")
public void testLazyRecursionBug() throws Exception { public void testLazyRecursionBug() throws Exception {
runTest("plugins/kotlin-serialization/kotlin-serialization-compiler/testData/diagnostics/LazyRecursionBug.kt"); runTest("plugins/kotlin-serialization/kotlin-serialization-compiler/testData/diagnostics/LazyRecursionBug.kt");
@@ -0,0 +1,12 @@
// !DIAGNOSTICS: -UNUSED_PARAMETER,-UNUSED_VARIABLE
// WITH_RUNTIME
// SKIP_TXT
import kotlinx.serialization.Serializable
import java.io.Serializable as JavaSerializable
@Serializable
class Data(val x: Int, <!INCORRECT_TRANSIENT!>@Transient<!> val y: String)
@Serializable
class Data2(val x: Int, @Transient val y: String) : JavaSerializable
@@ -0,0 +1,20 @@
// !DIAGNOSTICS: -UNUSED_PARAMETER,-UNUSED_VARIABLE
// WITH_RUNTIME
// SKIP_TXT
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.io.Serializable as JavaSerializable
import kotlin.jvm.Transient as JavaTransient
@Serializable
class Data(val x: Int, @Transient val y: String = "a")
@Serializable
class Data2(val x: Int, @Transient val y: String = "a") : JavaSerializable
@Serializable
class Data3(val x: Int, @Transient @JavaTransient val y: String = "a") : JavaSerializable
@Serializable
class Data4(val x: Int, <!INCORRECT_TRANSIENT!>@JavaTransient<!> val y: String)
@@ -15,6 +15,9 @@ dependencies {
compile(project(":idea:idea-maven")) compile(project(":idea:idea-maven"))
compile(project(":plugins:annotation-based-compiler-plugins-ide-support")) compile(project(":plugins:annotation-based-compiler-plugins-ide-support"))
compileOnly(intellijDep()) compileOnly(intellijDep())
Platform[192].orHigher {
compileOnly(intellijPluginDep("java"))
}
excludeInAndroidStudio(rootProject) { compileOnly(intellijPluginDep("maven")) } excludeInAndroidStudio(rootProject) { compileOnly(intellijPluginDep("maven")) }
compileOnly(intellijPluginDep("gradle")) compileOnly(intellijPluginDep("gradle"))
@@ -30,5 +33,5 @@ sourceSets {
runtimeJar() runtimeJar()
projectTest(parallel = true) { projectTest(parallel = true) {
} }
@@ -0,0 +1,42 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlinx.serialization.idea.quickfixes
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.diagnostics.Diagnostic
import org.jetbrains.kotlin.idea.quickfix.KotlinQuickFixAction
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory
import org.jetbrains.kotlin.idea.util.ImportInsertHelperImpl
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationErrors
import org.jetbrains.kotlinx.serialization.compiler.resolve.SerializationAnnotations
internal class AddKotlinxSerializationTransientImportQuickFix(expression: PsiElement) :
KotlinQuickFixAction<PsiElement>(expression) {
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
// Do not use Helper itself because it does not insert import on conflict;
// so it will always fail because k.jvm.Transient is always in auto-import
ImportInsertHelperImpl.addImport(project, file, SerializationAnnotations.serialTransientFqName, allUnder = false)
}
override fun getFamilyName(): String = text
override fun getText(): String = "Import ${SerializationAnnotations.serialTransientFqName}"
object Factory : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
if (diagnostic.factory != SerializationErrors.INCORRECT_TRANSIENT) return null
val castedDiagnostic = SerializationErrors.INCORRECT_TRANSIENT.cast(diagnostic)
val element = castedDiagnostic.psiElement
return AddKotlinxSerializationTransientImportQuickFix(element)
}
}
}
@@ -0,0 +1,16 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlinx.serialization.idea.quickfixes
import org.jetbrains.kotlin.idea.quickfix.QuickFixContributor
import org.jetbrains.kotlin.idea.quickfix.QuickFixes
import org.jetbrains.kotlinx.serialization.compiler.diagnostic.SerializationErrors
class SerializationQuickFixContributor : QuickFixContributor {
override fun registerQuickFixes(quickFixes: QuickFixes) {
quickFixes.register(SerializationErrors.INCORRECT_TRANSIENT, AddKotlinxSerializationTransientImportQuickFix.Factory)
}
}