From 1328e6ec772e181275c8bd12ccc9f992d0df440e Mon Sep 17 00:00:00 2001 From: stefankoppier Date: Thu, 21 Nov 2024 20:18:17 +0100 Subject: [PATCH 1/4] Added useful error when the mapping dsl is used as an extension- or dispatch receiver --- .../kotlin/tech/mappie/util/Identifiers.kt | 7 ++- .../mappie/validation/MappingValidation.kt | 1 + .../classes/CompileTimeReceiverDslProblems.kt | 56 +++++++++++++++++++ .../mappie/testing/objects/FromValueTest.kt | 44 +++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 compiler-plugin/src/main/kotlin/tech/mappie/validation/problems/classes/CompileTimeReceiverDslProblems.kt 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..fa176f88 --- /dev/null +++ b/compiler-plugin/src/main/kotlin/tech/mappie/validation/problems/classes/CompileTimeReceiverDslProblems.kt @@ -0,0 +1,56 @@ +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 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), + ) + } + + 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..6433b665 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,48 @@ 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") + } + } + + @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 From 9e07310487555f78a286aa2f21f20f3d25d7f48f Mon Sep 17 00:00:00 2001 From: stefankoppier Date: Thu, 21 Nov 2024 20:19:48 +0100 Subject: [PATCH 2/4] Updated changelog --- website/src/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) 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: From eaa8f6263bb4ea6d6335bb2a0e96d7f85d00d686 Mon Sep 17 00:00:00 2001 From: stefankoppier Date: Thu, 21 Nov 2024 20:23:21 +0100 Subject: [PATCH 3/4] Run SonarQube only on main branch --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) 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 }} From 8990a5d63edd7adc611445f5442cb4be52afe364 Mon Sep 17 00:00:00 2001 From: stefankoppier Date: Thu, 21 Nov 2024 20:30:39 +0100 Subject: [PATCH 4/4] Added suggestion when using run instead of kotlin.run --- .../problems/classes/CompileTimeReceiverDslProblems.kt | 6 ++++++ .../kotlin/tech/mappie/testing/objects/FromValueTest.kt | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) 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 index fa176f88..8d11bace 100644 --- 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 @@ -7,6 +7,7 @@ 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 @@ -24,6 +25,11 @@ class CompileTimeReceiverDslProblems private constructor( 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?") + } + } ) } 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 6433b665..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 @@ -91,7 +91,12 @@ class FromValueTest { ) } satisfies { isCompilationError() - hasErrorMessage(6, "The function run was called as an extension method on the mapping dsl which does not exist after compilation") + 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?" + ) + ) } }