[K2] Fix bug: generated 'copy' method from another module wasn't correctly recognized by FirDataClassCopyUsageWillBecomeInaccessibleChecker
KT-11914
Review: https://jetbrains.team/p/kt/reviews/15489/timeline
The bug: unfortunatelly, `origin` returns `Library` (instead of
`FirDeclarationOrigin.Synthetic.DataClassMember`) for declarations from
external modules, but the Checker only tested for
`FirDeclarationOrigin.Synthetic.DataClassMember`
Moreover, we already had the multi-module test that should have
prevented this bug from happening
`compiler/testData/diagnostics/tests/dataClassNonPublicConstructor/deprecationPhase2_internalConstructor.kt`
but there is another bug (or misunderstanding from my side?) in test
infrastructure. In `deprecationPhase2_internalConstructor`, `origin`
returns `FirDeclarationOrigin.Synthetic.DataClassMember` for unknown
reason.
That's why I introduce full blown up Gradle test
diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/expression/FirDataClassCopyUsageWillBecomeInaccessibleChecker.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/expression/FirDataClassCopyUsageWillBecomeInaccessibleChecker.kt
index 1e6d130..666241b 100644
--- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/expression/FirDataClassCopyUsageWillBecomeInaccessibleChecker.kt
+++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/expression/FirDataClassCopyUsageWillBecomeInaccessibleChecker.kt
@@ -7,16 +7,21 @@
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.diagnostics.reportOn
+import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.analysis.checkers.MppCheckerKind
import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext
import org.jetbrains.kotlin.fir.analysis.checkers.declaration.primaryConstructorSymbol
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors
import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin
import org.jetbrains.kotlin.fir.declarations.FirMemberDeclaration
+import org.jetbrains.kotlin.fir.declarations.utils.isData
import org.jetbrains.kotlin.fir.expressions.FirFunctionCall
import org.jetbrains.kotlin.fir.references.symbol
import org.jetbrains.kotlin.fir.symbols.SymbolInternals
import org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol
+import org.jetbrains.kotlin.fir.symbols.impl.FirNamedFunctionSymbol
+import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol
+import org.jetbrains.kotlin.fir.types.classId
import org.jetbrains.kotlin.fir.types.resolvedType
import org.jetbrains.kotlin.fir.types.toRegularClassSymbol
import org.jetbrains.kotlin.fir.visibilityChecker
@@ -24,9 +29,9 @@
object FirDataClassCopyUsageWillBecomeInaccessibleChecker : FirFunctionCallChecker(MppCheckerKind.Common) {
override fun check(expression: FirFunctionCall, context: CheckerContext, reporter: DiagnosticReporter) {
- val dataClass = expression.resolvedType.toRegularClassSymbol(context.session) ?: return
+ val dataClass = expression.resolvedType.toRegularClassSymbol(context.session)?.takeIf { it.isData } ?: return
val copyFunction = expression.calleeReference.symbol as? FirCallableSymbol ?: return
- if (DataClassResolver.isCopy(copyFunction.name) && copyFunction.origin == FirDeclarationOrigin.Synthetic.DataClassMember) {
+ if (copyFunction.isDataClassCopy(dataClass, context.session)) {
val dataClassConstructor = dataClass.primaryConstructorSymbol(context.session) ?: return
@OptIn(SymbolInternals::class)
@@ -57,3 +62,18 @@
}
}
}
+
+fun FirCallableSymbol<*>.isDataClassCopy(dataClass: FirRegularClassSymbol, session: FirSession): Boolean {
+ if (DataClassResolver.isCopy(name) && this is FirNamedFunctionSymbol) {
+ if (origin == FirDeclarationOrigin.Synthetic.DataClassMember) {
+ return true
+ }
+ val constructor = dataClass.primaryConstructorSymbol(session)
+ if (constructor != null && origin == FirDeclarationOrigin.Library && receiverParameter == null &&
+ valueParameterSymbols.map { it.resolvedReturnType.classId } == constructor.valueParameterSymbols.map { it.resolvedReturnType.classId }
+ ) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/SimpleKotlinGradleIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/SimpleKotlinGradleIT.kt
index e579fcf..38fe60f 100644
--- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/SimpleKotlinGradleIT.kt
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/SimpleKotlinGradleIT.kt
@@ -90,6 +90,15 @@
}
@GradleTest
+ fun testDataClassInternalConstructorUsageWillBecomeInaccessible(gradleVersion: GradleVersion) {
+ project("dataClassInternalConstructorUsageWillBecomeInaccessible", gradleVersion) {
+ buildAndFail("assemble") {
+ assertOutputContains("This 'copy()' exposes the non-public primary constructor of a 'data class'. Please migrate the usage.")
+ }
+ }
+ }
+
+ @GradleTest
@DisplayName("Should produce '.kotlin_module' file with specified name")
fun testModuleName(gradleVersion: GradleVersion) {
project("moduleName", gradleVersion) {
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/build.gradle.kts
new file mode 100644
index 0000000..0e6aecb
--- /dev/null
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/build.gradle.kts
@@ -0,0 +1,20 @@
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
+import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
+
+plugins {
+ kotlin("jvm")
+}
+
+repositories {
+ mavenLocal()
+ mavenCentral()
+}
+
+dependencies {
+ implementation(project(":lib"))
+}
+
+tasks.withType<KotlinJvmCompile>().configureEach {
+ compilerOptions.progressiveMode.set(true)
+ compilerOptions.languageVersion.set(KotlinVersion.KOTLIN_2_0)
+}
\ No newline at end of file
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/lib/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/lib/build.gradle.kts
new file mode 100644
index 0000000..6a6b756
--- /dev/null
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/lib/build.gradle.kts
@@ -0,0 +1,15 @@
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
+import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
+
+plugins {
+ kotlin("jvm")
+}
+
+repositories {
+ mavenLocal()
+ mavenCentral()
+}
+
+tasks.withType<KotlinJvmCompile>().configureEach {
+ compilerOptions.languageVersion.set(KotlinVersion.KOTLIN_2_0)
+}
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/lib/src/main/kotlin/Lib.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/lib/src/main/kotlin/Lib.kt
new file mode 100644
index 0000000..6b14ef5
--- /dev/null
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/lib/src/main/kotlin/Lib.kt
@@ -0,0 +1,6 @@
+@ExposedCopyVisibility
+data class Foo private constructor(val x: Int) {
+ companion object {
+ fun new() = Foo(1)
+ }
+}
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/settings.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/settings.gradle.kts
new file mode 100644
index 0000000..408a5e7
--- /dev/null
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/settings.gradle.kts
@@ -0,0 +1 @@
+include(":lib")
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/src/main/kotlin/main.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/src/main/kotlin/main.kt
new file mode 100644
index 0000000..79adf32
--- /dev/null
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/dataClassInternalConstructorUsageWillBecomeInaccessible/src/main/kotlin/main.kt
@@ -0,0 +1,3 @@
+fun main() {
+ Foo.new().copy()
+}