Skip to content

Commit 2807a02

Browse files
authored
fix(#180): respect source and target class visibilities for extension function generation (#181)
1 parent db9bae9 commit 2807a02

File tree

7 files changed

+413
-0
lines changed

7 files changed

+413
-0
lines changed

processor/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dependencies {
2121

2222
testImplementation(project(":annotations"))
2323
testImplementation(project(":converter"))
24+
testImplementation("com.github.dpaukov:combinatoricslib3:${Versions.combinatoricslib3}")
2425
testImplementation("org.junit.jupiter:junit-jupiter-api:${Versions.jUnit}")
2526
testImplementation("org.junit.jupiter:junit-jupiter-params:${Versions.jUnit}")
2627
testImplementation(kotlinTest)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.mcarle.konvert.processor.exceptions
2+
3+
import com.google.devtools.ksp.symbol.KSClassDeclaration
4+
import com.google.devtools.ksp.symbol.Visibility
5+
6+
class InaccessibleDueToVisibilityClassException(
7+
visibility: Visibility,
8+
classDeclaration: KSClassDeclaration
9+
) : RuntimeException(
10+
"The class ${(classDeclaration.qualifiedName ?: classDeclaration.simpleName).asString()} is not accessible due to its $visibility visibility"
11+
)

processor/src/main/kotlin/io/mcarle/konvert/processor/extensions.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.google.devtools.ksp.symbol.KSAnnotation
55
import com.google.devtools.ksp.symbol.KSClassDeclaration
66
import com.google.devtools.ksp.symbol.KSNode
77
import com.google.devtools.ksp.symbol.KSValueParameter
8+
import com.google.devtools.ksp.symbol.Visibility
89
import io.mcarle.konvert.api.Konfig
910
import io.mcarle.konvert.api.Mapping
1011
import io.mcarle.konvert.api.NoParamDefinedException
@@ -63,3 +64,16 @@ fun Konfig.Companion.from(annotation: KSAnnotation) = Konfig(
6364
)
6465

6566
fun KSValueParameter.typeClassDeclaration(): KSClassDeclaration? = this.type.resolve().classDeclaration()
67+
68+
fun Visibility.isEqualOrMoreRestrictedThan(other: Visibility): Boolean {
69+
if (this == other) return true
70+
71+
return when (this) {
72+
Visibility.PUBLIC -> false
73+
Visibility.JAVA_PACKAGE -> other == Visibility.PUBLIC
74+
Visibility.INTERNAL -> other == Visibility.PUBLIC || other == Visibility.JAVA_PACKAGE
75+
Visibility.PROTECTED -> other != Visibility.LOCAL && other != Visibility.PRIVATE
76+
Visibility.LOCAL -> other != Visibility.PRIVATE
77+
Visibility.PRIVATE -> true
78+
}
79+
}

processor/src/main/kotlin/io/mcarle/konvert/processor/konvertfrom/KonvertFromCodeGenerator.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package io.mcarle.konvert.processor.konvertfrom
22

3+
import com.google.devtools.ksp.getVisibility
34
import com.google.devtools.ksp.processing.KSPLogger
45
import com.google.devtools.ksp.processing.Resolver
6+
import com.google.devtools.ksp.symbol.KSClassDeclaration
7+
import com.google.devtools.ksp.symbol.Visibility
58
import com.squareup.kotlinpoet.FunSpec
9+
import com.squareup.kotlinpoet.KModifier
610
import com.squareup.kotlinpoet.ksp.toTypeName
711
import io.mcarle.konvert.converter.api.config.withIsolatedConfiguration
812
import io.mcarle.konvert.processor.codegen.CodeBuilder
913
import io.mcarle.konvert.processor.codegen.CodeGenerator
1014
import io.mcarle.konvert.processor.codegen.MappingContext
15+
import io.mcarle.konvert.processor.exceptions.InaccessibleDueToVisibilityClassException
1116
import io.mcarle.konvert.processor.exceptions.KonvertException
17+
import io.mcarle.konvert.processor.isEqualOrMoreRestrictedThan
1218
import io.mcarle.konvert.processor.validated
1319

1420
object KonvertFromCodeGenerator {
@@ -27,6 +33,13 @@ object KonvertFromCodeGenerator {
2733

2834
codeBuilder.addFunction(
2935
funBuilder = FunSpec.builder(data.mapFunctionName)
36+
.addModifiers(
37+
*determineModifiers(
38+
data.sourceClassDeclaration,
39+
data.targetClassDeclaration,
40+
data.targetCompanionDeclaration
41+
)
42+
)
3043
.returns(data.targetClassDeclaration.asStarProjectedType().toTypeName())
3144
.addParameter(data.paramName, data.sourceClassDeclaration.asStarProjectedType().toTypeName())
3245
.receiver(data.targetCompanionDeclaration.asStarProjectedType().toTypeName())
@@ -66,4 +79,41 @@ object KonvertFromCodeGenerator {
6679
)
6780
}
6881

82+
private fun determineModifiers(
83+
sourceClassDeclaration: KSClassDeclaration,
84+
targetClassDeclaration: KSClassDeclaration,
85+
targetCompanionDeclaration: KSClassDeclaration
86+
): Array<KModifier> {
87+
val sourceVisibility = sourceClassDeclaration.getVisibility()
88+
val targetClassVisibility = targetClassDeclaration.getVisibility()
89+
val targetCompanionVisibility = targetCompanionDeclaration.getVisibility()
90+
91+
val (moreRestrictedVisibility, moreRestrictedClassDeclaration) =
92+
if (targetCompanionVisibility.isEqualOrMoreRestrictedThan(targetClassVisibility)) {
93+
if (sourceVisibility.isEqualOrMoreRestrictedThan(targetCompanionVisibility)) {
94+
sourceVisibility to sourceClassDeclaration
95+
} else {
96+
targetCompanionVisibility to targetCompanionDeclaration
97+
}
98+
} else {
99+
if (sourceVisibility.isEqualOrMoreRestrictedThan(targetClassVisibility)) {
100+
sourceVisibility to sourceClassDeclaration
101+
} else {
102+
targetClassVisibility to targetClassDeclaration
103+
}
104+
}
105+
106+
return when (moreRestrictedVisibility) {
107+
Visibility.PUBLIC -> arrayOf(KModifier.PUBLIC)
108+
Visibility.JAVA_PACKAGE,
109+
Visibility.INTERNAL -> arrayOf(KModifier.INTERNAL)
110+
Visibility.PROTECTED,
111+
Visibility.LOCAL,
112+
Visibility.PRIVATE -> throw InaccessibleDueToVisibilityClassException(
113+
visibility = moreRestrictedVisibility,
114+
classDeclaration = moreRestrictedClassDeclaration
115+
)
116+
}
117+
}
118+
69119
}

processor/src/main/kotlin/io/mcarle/konvert/processor/konvertto/KonvertToCodeGenerator.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package io.mcarle.konvert.processor.konvertto
22

3+
import com.google.devtools.ksp.getVisibility
34
import com.google.devtools.ksp.processing.KSPLogger
45
import com.google.devtools.ksp.processing.Resolver
6+
import com.google.devtools.ksp.symbol.KSClassDeclaration
7+
import com.google.devtools.ksp.symbol.Visibility
58
import com.squareup.kotlinpoet.FunSpec
9+
import com.squareup.kotlinpoet.KModifier
610
import com.squareup.kotlinpoet.ksp.toTypeName
711
import io.mcarle.konvert.converter.api.config.withIsolatedConfiguration
812
import io.mcarle.konvert.processor.codegen.CodeBuilder
913
import io.mcarle.konvert.processor.codegen.CodeGenerator
1014
import io.mcarle.konvert.processor.codegen.MappingContext
15+
import io.mcarle.konvert.processor.exceptions.InaccessibleDueToVisibilityClassException
1116
import io.mcarle.konvert.processor.exceptions.KonvertException
17+
import io.mcarle.konvert.processor.isEqualOrMoreRestrictedThan
1218
import io.mcarle.konvert.processor.validated
1319

1420
object KonvertToCodeGenerator {
@@ -27,6 +33,7 @@ object KonvertToCodeGenerator {
2733

2834
fileSpecBuilder.addFunction(
2935
funBuilder = FunSpec.builder(data.mapFunctionName)
36+
.addModifiers(*determineModifiers(data.sourceClassDeclaration, data.targetClassDeclaration))
3037
.returns(data.targetClassDeclaration.asStarProjectedType().toTypeName())
3138
.receiver(data.sourceClassDeclaration.asStarProjectedType().toTypeName())
3239
.addCode(
@@ -65,4 +72,31 @@ object KonvertToCodeGenerator {
6572
)
6673
}
6774

75+
private fun determineModifiers(
76+
sourceClassDeclaration: KSClassDeclaration,
77+
targetClassDeclaration: KSClassDeclaration
78+
): Array<KModifier> {
79+
val sourceVisibility = sourceClassDeclaration.getVisibility()
80+
val targetVisibility = targetClassDeclaration.getVisibility()
81+
82+
val (moreRestrictedVisibility, moreRestrictedClassDeclaration) =
83+
if (sourceVisibility.isEqualOrMoreRestrictedThan(targetVisibility)) {
84+
sourceVisibility to sourceClassDeclaration
85+
} else {
86+
targetVisibility to targetClassDeclaration
87+
}
88+
89+
return when (moreRestrictedVisibility) {
90+
Visibility.PUBLIC -> arrayOf(KModifier.PUBLIC)
91+
Visibility.JAVA_PACKAGE,
92+
Visibility.INTERNAL -> arrayOf(KModifier.INTERNAL)
93+
Visibility.PROTECTED,
94+
Visibility.LOCAL,
95+
Visibility.PRIVATE -> throw InaccessibleDueToVisibilityClassException(
96+
visibility = moreRestrictedVisibility,
97+
classDeclaration = moreRestrictedClassDeclaration
98+
)
99+
}
100+
}
101+
68102
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package io.mcarle.konvert.processor.konvertfrom
2+
3+
import com.tschuchort.compiletesting.KotlinCompilation
4+
import com.tschuchort.compiletesting.SourceFile
5+
import io.mcarle.konvert.converter.SameTypeConverter
6+
import io.mcarle.konvert.processor.KonverterITest
7+
import io.mcarle.konvert.processor.exceptions.InaccessibleDueToVisibilityClassException
8+
import io.mcarle.konvert.processor.generatedSourceFor
9+
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
10+
import org.junit.jupiter.params.ParameterizedTest
11+
import org.junit.jupiter.params.provider.Arguments
12+
import org.junit.jupiter.params.provider.MethodSource
13+
import org.paukov.combinatorics3.Generator
14+
import org.paukov.combinatorics3.IGenerator
15+
import kotlin.test.assertContains
16+
17+
@OptIn(ExperimentalCompilerApi::class)
18+
class KonvertFromVisibilityITest : KonverterITest() {
19+
20+
companion object {
21+
@JvmStatic
22+
private fun possibleCombinations(): IGenerator<List<String>> = Generator.cartesianProduct(
23+
// source class (can be java)
24+
listOf(
25+
"public", "private", "internal", "java:public", "java:",
26+
),
27+
// target class (cannot be java)
28+
listOf(
29+
"public", "private", "internal"
30+
),
31+
// target class companion (cannot be java)
32+
listOf(
33+
"public", "private", "internal", "protected"
34+
)
35+
)
36+
37+
private fun Iterable<List<String>>.toParameters(): List<Arguments> = this.map {
38+
val sourceClassVisibility = it[0]
39+
val targetClassVisibility = it[1]
40+
val targetClassCompanionVisibility = it[2]
41+
Arguments.of(sourceClassVisibility, targetClassVisibility, targetClassCompanionVisibility)
42+
}
43+
44+
private fun Iterable<List<String>>.filterValid(valid: Boolean) = this.filter {
45+
val sourceClassVisibility = it[0]
46+
val targetClassVisibility = it[1]
47+
val targetClassCompanionVisibility = it[2]
48+
49+
if (sourceClassVisibility == "private"
50+
|| targetClassVisibility == "private"
51+
|| targetClassCompanionVisibility == "private"
52+
) {
53+
return@filter !valid
54+
}
55+
56+
if (targetClassCompanionVisibility == "protected") {
57+
return@filter !valid
58+
}
59+
60+
valid
61+
}
62+
63+
@JvmStatic
64+
fun validCombinations(): List<Arguments> = possibleCombinations().filterValid(true).toParameters()
65+
66+
@JvmStatic
67+
fun invalidCombinations(): List<Arguments> = possibleCombinations().filterValid(false).toParameters()
68+
}
69+
70+
@ParameterizedTest
71+
@MethodSource("validCombinations")
72+
fun checkCorrectVisibilityGenerated(
73+
sourceClassVisibility: String,
74+
targetClassVisibility: String,
75+
targetClassCompanionVisibility: String
76+
) {
77+
val (compilation) = compileWith(
78+
enabledConverters = listOf(SameTypeConverter()),
79+
expectResultCode = KotlinCompilation.ExitCode.OK,
80+
code = arrayOf(
81+
generateSourceFile(sourceClassVisibility),
82+
generateTargetFile(targetClassVisibility, targetClassCompanionVisibility)
83+
)
84+
)
85+
86+
val extensionFunctionCode = compilation.generatedSourceFor("TargetClassKonverter.kt")
87+
88+
val expectedVisibilityModifier = if (sourceClassVisibility.contains("public")
89+
&& targetClassVisibility == "public"
90+
&& targetClassCompanionVisibility == "public"
91+
) "public" else "internal"
92+
93+
assertContains(extensionFunctionCode, "$expectedVisibilityModifier fun TargetClass.Companion.fromSourceClass")
94+
}
95+
96+
@ParameterizedTest
97+
@MethodSource("invalidCombinations")
98+
fun invalidCombinationsReportedWithInaccessibleDueToVisibilityClassException(
99+
sourceClassVisibility: String,
100+
targetClassVisibility: String,
101+
targetClassCompanionVisibility: String
102+
) {
103+
val (_, compilationResult) = compileWith(
104+
enabledConverters = listOf(SameTypeConverter()),
105+
expectResultCode = KotlinCompilation.ExitCode.COMPILATION_ERROR,
106+
code = arrayOf(
107+
generateSourceFile(sourceClassVisibility),
108+
generateTargetFile(targetClassVisibility, targetClassCompanionVisibility)
109+
)
110+
)
111+
112+
assertContains(compilationResult.messages, "${InaccessibleDueToVisibilityClassException::class.simpleName}")
113+
}
114+
115+
private fun generateSourceFile(sourceClassVisibility: String): SourceFile {
116+
return if (sourceClassVisibility.startsWith("java:")) {
117+
val effectiveSourceClassVisibility = sourceClassVisibility.removePrefix("java:")
118+
SourceFile.java(
119+
name = "SourceClass.java",
120+
contents =
121+
"""
122+
$effectiveSourceClassVisibility class SourceClass {
123+
private String property;
124+
125+
public SourceClass(String property) {
126+
this.property = property;
127+
}
128+
129+
@org.jetbrains.annotations.NotNull
130+
public String getProperty() {
131+
return property;
132+
}
133+
}
134+
""".trimIndent()
135+
)
136+
} else {
137+
SourceFile.kotlin(
138+
name = "SourceClass.kt",
139+
contents =
140+
"""
141+
$sourceClassVisibility class SourceClass(val property: String)
142+
""".trimIndent()
143+
)
144+
}
145+
}
146+
147+
private fun generateTargetFile(targetClassVisibility: String, targetClassCompanionVisibility: String): SourceFile {
148+
return SourceFile.kotlin(
149+
name = "TargetClass.kt",
150+
contents =
151+
"""
152+
import io.mcarle.konvert.api.KonvertFrom
153+
154+
@KonvertFrom(SourceClass::class)
155+
$targetClassVisibility class TargetClass(val property: String) {
156+
$targetClassCompanionVisibility companion object
157+
}
158+
""".trimIndent()
159+
)
160+
}
161+
162+
}

0 commit comments

Comments
 (0)