diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d18beb66..86cb7038 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,9 +39,11 @@ jobs: run: ./gradlew build - name: Coverage Report + if: github.ref == 'refs/heads/main' run: ./gradlew testCodeCoverageReport - name: Sonar + if: github.ref == 'refs/heads/main' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/util/Identifiers.kt b/compiler-plugin/src/main/kotlin/tech/mappie/util/Identifiers.kt index 2203dbfb..efbea887 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/util/Identifiers.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/util/Identifiers.kt @@ -1,5 +1,6 @@ package tech.mappie.util +import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name @@ -31,4 +32,8 @@ val IDENTIFIER_TRANSFORM = Name.identifier("transform") val IDENTIFIER_VIA = Name.identifier("via") -val PACKAGE_TECH_MAPPIE_API_CONFIG = FqName("tech.mappie.api.config") \ No newline at end of file +val PACKAGE_TECH_MAPPIE_API = FqName("tech.mappie.api") + +val PACKAGE_TECH_MAPPIE_API_CONFIG = FqName("tech.mappie.api.config") + +val CLASS_ID_OBJECT_MAPPING_CONSTRUCTOR = ClassId(PACKAGE_TECH_MAPPIE_API, Name.identifier("ObjectMappingConstructor")) diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt b/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt index 26ae2a59..83fc88ce 100644 --- a/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt +++ b/compiler-plugin/src/main/kotlin/tech/mappie/validation/MappingValidation.kt @@ -33,6 +33,7 @@ interface MappingValidation { addAll(MapperGenerationRequestProblems.of(context, mapping).all()) addAll(ClassConfigProblems.of(context, mapping).all()) addAll(UnnecessaryFromPropertyNotNullProblems.of(context, mapping).all()) + addAll(CompileTimeReceiverDslProblems.of(context, mapping).all()) } } diff --git a/compiler-plugin/src/main/kotlin/tech/mappie/validation/problems/classes/CompileTimeReceiverDslProblems.kt b/compiler-plugin/src/main/kotlin/tech/mappie/validation/problems/classes/CompileTimeReceiverDslProblems.kt new file mode 100644 index 00000000..8d11bace --- /dev/null +++ b/compiler-plugin/src/main/kotlin/tech/mappie/validation/problems/classes/CompileTimeReceiverDslProblems.kt @@ -0,0 +1,62 @@ +package tech.mappie.validation.problems.classes + +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.types.classOrNull +import org.jetbrains.kotlin.ir.util.fileEntry +import tech.mappie.resolving.ClassMappingRequest +import tech.mappie.resolving.classes.sources.ValueMappingSource +import tech.mappie.util.CLASS_ID_OBJECT_MAPPING_CONSTRUCTOR +import tech.mappie.util.location +import org.jetbrains.kotlin.name.Name +import tech.mappie.validation.Problem +import tech.mappie.validation.ValidationContext + +class CompileTimeReceiverDslProblems private constructor( + private val context: ValidationContext, + private val mappings: List>, +) { + private enum class ProblemSource { EXTENSION, DISPATCH } + + fun all(): List = mappings.map { (call, source) -> + val name = call.symbol.owner.name.asString() + Problem.error( + when (source) { + ProblemSource.EXTENSION -> "The function $name was called as an extension method on the mapping dsl which does not exist after compilation" + ProblemSource.DISPATCH -> "The function $name was called on the mapping dsl which does not exist after compilation" + }, + location(context.function.fileEntry, call), + buildList { + if (call.symbol.owner.name == Name.identifier("run")) { + add("Did you mean to use kotlin.run?") + } + } + ) + } + + companion object { + fun of(context: ValidationContext, mapping: ClassMappingRequest): CompileTimeReceiverDslProblems { + val mappings = mapping.mappings.values + .filter { it.size == 1 } + .map { it.single() } + .filterIsInstance() + .map { it.expression } + .filterIsInstance() + + val dispatch = mappings + .filter { it.hasIncorrectDispatchReceiver(context) } + .map { it to ProblemSource.DISPATCH } + + val extension = mappings + .filter { it.hasIncorrectExtensionReceiver(context) } + .map { it to ProblemSource.EXTENSION } + + return CompileTimeReceiverDslProblems(context, dispatch + extension) + } + + private fun IrCall.hasIncorrectExtensionReceiver(context: ValidationContext) = + extensionReceiver?.type?.classOrNull == context.pluginContext.referenceClass(CLASS_ID_OBJECT_MAPPING_CONSTRUCTOR) + + private fun IrCall.hasIncorrectDispatchReceiver(context: ValidationContext) = + dispatchReceiver?.type?.classOrNull == context.pluginContext.referenceClass(CLASS_ID_OBJECT_MAPPING_CONSTRUCTOR) + } +} \ No newline at end of file diff --git a/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects/FromValueTest.kt b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects/FromValueTest.kt index a014876f..5aa0a16d 100644 --- a/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects/FromValueTest.kt +++ b/compiler-plugin/src/test/kotlin/tech/mappie/testing/objects/FromValueTest.kt @@ -71,4 +71,53 @@ class FromValueTest { assertThat(mapper.map(Unit)).isEqualTo(Output(null)) } } + + @Test + fun `map property fromValue using extension receiver on mapping dsl should fail`() { + compile(directory) { + file("Test.kt", + """ + import tech.mappie.api.ObjectMappie + import tech.mappie.testing.objects.FromValueTest.* + + class Mapper : ObjectMappie() { + override fun map(from: Unit) = mapping { + Output::value fromValue run { + "test" + } + } + } + """ + ) + } satisfies { + isCompilationError() + hasErrorMessage(6, + "The function run was called as an extension method on the mapping dsl which does not exist after compilation", + listOf( + "Did you mean to use kotlin.run?" + ) + ) + } + } + + @Test + fun `map property fromValue using dispatch receiver on mapping dsl should fail`() { + compile(directory) { + file("Test.kt", + """ + import tech.mappie.api.ObjectMappie + import tech.mappie.testing.objects.FromValueTest.* + + class Mapper : ObjectMappie() { + override fun map(from: Unit) = mapping { + Output::value fromValue toString() + } + } + """ + ) + } satisfies { + isCompilationError() + hasErrorMessage(6, "The function toString was called on the mapping dsl which does not exist after compilation") + } + } } \ No newline at end of file diff --git a/website/src/changelog.md b/website/src/changelog.md index 509950b4..4e6a8019 100644 --- a/website/src/changelog.md +++ b/website/src/changelog.md @@ -2,6 +2,10 @@ title: "Changelog" layout: "layouts/changelog.html" changelog: + - date: "tbd" + title: "v0.10.0" + items: + - "Added an explicit error message when the compile-time mapping dsl is used during runtime." - date: "2024-11-18" title: "v0.9.2" items: