Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package io.mcarle.konvert.converter.api

import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSType
import com.squareup.kotlinpoet.CodeBlock
import io.mcarle.konvert.converter.api.config.Configuration
import io.mcarle.konvert.converter.api.config.enforceNotNull
import io.mcarle.konvert.converter.api.config.enforceNotNullStrategy
import io.mcarle.konvert.converter.api.config.EnforceNotNullStrategy

abstract class AbstractTypeConverter(name: String? = null) : TypeConverter {
override val name: String = name ?: this::class.java.simpleName
Expand All @@ -29,10 +32,48 @@ abstract class AbstractTypeConverter(name: String? = null) : TypeConverter {
return source.isNullable() && !target.isNullable()
}

/**
* Wraps the given [fieldName] according to the not-null enforcement configuration.
*
* - if no enforcement is needed: returns [fieldName] as is
* - if enforcement is needed and strategy is [EnforceNotNullStrategy.ASSERTION_OPERATOR]:
* generates `expression!!`
* - if enforcement is needed and strategy is [EnforceNotNullStrategy.REQUIRE_NOT_NULL]:
* generates `requireNotNull(expression) { "Value for '<expression>' must not be null" }`
*/
protected fun applyNotNullEnforcementIfNeeded(
expression: CodeBlock,
fieldName: String?,
source: KSType,
target: KSType
): CodeBlock {
val needsNotNull = needsNotNullAssertionOperator(source, target)
if (!needsNotNull) {
return expression
}

if (!Configuration.enforceNotNull) {
throw IllegalStateException(
"Not-null enforcement is required to map from nullable '$source' to non-nullable '$target', " +
"but '${io.mcarle.konvert.converter.api.config.ENFORCE_NOT_NULL_OPTION.key}' is set to false."
)
}

return when (Configuration.enforceNotNullStrategy) {
EnforceNotNullStrategy.ASSERTION_OPERATOR ->
CodeBlock.of("%L!!", expression)

fun appendNotNullAssertionOperatorIfNeeded(source: KSType, target: KSType) = if (needsNotNullAssertionOperator(source, target)) {
"!!"
} else {
""
EnforceNotNullStrategy.REQUIRE_NOT_NULL -> {
val message = fieldName
?.let { "Value for '$it' must not be null" }
?: "Value must not be null"
Comment on lines +68 to +69
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message construction doesn't match the test expectation. Test line 84 expects the format 'it.test0' must not be null but this produces Value for 'it.test0' must not be null. Consider using just \"$it must not be null\" to match test expectations and provide more concise error messages.

Suggested change
?.let { "Value for '$it' must not be null" }
?: "Value must not be null"
?.let { "'$it' must not be null" }
?: "must not be null"

Copilot uses AI. Check for mistakes.
CodeBlock.of("requireNotNull(%L) { %S }", expression, message)
}
}
}


@Deprecated("Use applyNotNullEnforcementIfNeeded instead", ReplaceWith("applyNotNullEnforcementIfNeeded(fieldName, source, target)"))
fun appendNotNullAssertionOperatorIfNeeded(source: KSType, target: KSType) =
if (needsNotNullAssertionOperator(source, target)) "!!" else ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.mcarle.konvert.converter.api.config

/**
* Strategy used when `konvert.enforce-not-null = true` and Konvert has to map
* from a nullable source type to a non-nullable target type.
*
* - [ASSERTION_OPERATOR] uses the Kotlin `!!` operator and throws a raw [NullPointerException].
* - [REQUIRE_NOT_NULL] wraps the access in [requireNotNull] and throws an [IllegalArgumentException]
* with a descriptive message generated by the processor.
*/
enum class EnforceNotNullStrategy {

/**
* Use the Kotlin not-null assertion operator (`!!`).
*
* This strategy is kept for backwards compatibility and may no longer be the default
* in a future Konvert version. Prefer [REQUIRE_NOT_NULL] for clearer error messages.
*/
@Deprecated(
message = "ASSERTION_OPERATOR is kept for backwards compatibility and may no longer be the default in a future Konvert version. Prefer REQUIRE_NOT_NULL.",
replaceWith = ReplaceWith("EnforceNotNullStrategy.REQUIRE_NOT_NULL")
)
ASSERTION_OPERATOR,

/**
* Use [requireNotNull] with a generated error message.
*/
REQUIRE_NOT_NULL
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ import java.util.UUID
*/
object ENFORCE_NOT_NULL_OPTION : Option<Boolean>("konvert.enforce-not-null", false)

/**
* Controls how Konvert enforces non-nullability when [ENFORCE_NOT_NULL_OPTION] is enabled.
*
* Possible values:
* - "assertion-operator" (default)
* - "require-not-null"
*
* @see EnforceNotNullStrategy
* @since 4.5.0
*/
object ENFORCE_NOT_NULL_STRATEGY_OPTION : Option<EnforceNotNullStrategy>("konvert.enforce-not-null-strategy", EnforceNotNullStrategy.ASSERTION_OPERATOR)

/**
* When set to true, a class instead of an object is being generated during processing of @[io.mcarle.konvert.api.Konverter]
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ import io.mcarle.konvert.api.TypeConverterName
val Configuration.Companion.enforceNotNull: Boolean
get() = ENFORCE_NOT_NULL_OPTION.get(CURRENT, String::toBoolean)

/**
* @see ENFORCE_NOT_NULL_STRATEGY_OPTION
*/
val Configuration.Companion.enforceNotNullStrategy: EnforceNotNullStrategy
get() = ENFORCE_NOT_NULL_STRATEGY_OPTION.get(CURRENT) { configString ->
when (configString.lowercase()) {
"assertion-operator" -> EnforceNotNullStrategy.ASSERTION_OPERATOR
"require-not-null" -> EnforceNotNullStrategy.REQUIRE_NOT_NULL
else ->
EnforceNotNullStrategy.entries.firstOrNull {
it.name.equals(configString, ignoreCase = true)
} ?: ENFORCE_NOT_NULL_STRATEGY_OPTION.defaultValue
}
}

/**
* @see KONVERTER_GENERATE_CLASS_OPTION
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ abstract class DateToTemporalConverter(
}

override fun convert(fieldName: String, source: KSType, target: KSType): CodeBlock {
val sourceNullable = source.isNullable()
val convertCode = convert(fieldName, if (sourceNullable) "?" else "")

return CodeBlock.of(
convertCode + appendNotNullAssertionOperatorIfNeeded(source, target)
val nc = if (source.isNullable()) "?" else ""
val expression = CodeBlock.of("%L", convert(fieldName, nc))

return applyNotNullEnforcementIfNeeded(
expression = expression,
fieldName = fieldName,
source = source,
target = target
)
}

Expand All @@ -46,3 +49,4 @@ class DateToInstantConverter : DateToTemporalConverter(Instant::class) {
override fun convert(fieldName: String, nc: String): String = "$fieldName$nc.toInstant()"
override val enabledByDefault = true
}

Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ abstract class DateToXConverter(
}

override fun convert(fieldName: String, source: KSType, target: KSType): CodeBlock {
val sourceNullable = source.isNullable()
val convertCode = convert(fieldName, if (sourceNullable) "?" else "")
val nc = if (source.isNullable()) "?" else ""
val expression = CodeBlock.of("%L", convert(fieldName, nc))

return CodeBlock.of(
convertCode + appendNotNullAssertionOperatorIfNeeded(source, target)
return applyNotNullEnforcementIfNeeded(
expression = expression,
fieldName = fieldName,
source = source,
target = target
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ abstract class EnumToXConverter(
}

override fun convert(fieldName: String, source: KSType, target: KSType): CodeBlock {
val sourceNullable = source.isNullable()
val convertCode = convert(fieldName, if (sourceNullable) "?" else "")

return CodeBlock.of(
convertCode + appendNotNullAssertionOperatorIfNeeded(source, target)
val nc = if (source.isNullable()) "?" else ""
val expression = CodeBlock.of("%L", convert(fieldName, nc))

return applyNotNullEnforcementIfNeeded(
expression = expression,
fieldName = fieldName,
source = source,
target = target
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,26 @@ abstract class IterableToXConverter(
}
args += mapSourceContainerCode

val code = mapSourceContentCode + "%L" + appendNotNullAssertionOperatorIfNeeded(source, target)
val code = mapSourceContentCode + "%L"

return CodeBlock.of(
if (castNeeded) {
args += target.toTypeName()
"($code·as·%T)" // encapsulate with braces
} else {
code
},
*args.toTypedArray()
val expression = if (castNeeded) {
args += target.toTypeName()
CodeBlock.of(
"($code·as·%T)",
*args.toTypedArray()
)
} else {
CodeBlock.of(
code,
*args.toTypedArray()
)
}

return applyNotNullEnforcementIfNeeded(
expression = expression,
fieldName = fieldName,
source = source,
target = target
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,19 +257,30 @@ newKey·to·newValue
}
args += mapSourceContainerCode

val code = mapSourceContentCode + "%L" + appendNotNullAssertionOperatorIfNeeded(source, target)
val code = mapSourceContentCode + "%L"

return CodeBlock.of(
if (changedTypes || castNeeded(genericSourceKeyType, genericTargetKeyType)) {
args += target.toTypeName()
"($code·as·%T)" // encapsulate with braces
} else {
code
},
*args.toTypedArray()
val baseExpression = if (changedTypes || castNeeded(genericSourceKeyType, genericTargetKeyType)) {
args += target.toTypeName()
CodeBlock.of(
"($code·as·%T)", // encapsulate with braces
*args.toTypedArray()
)
} else {
CodeBlock.of(
code,
*args.toTypedArray()
)
}

return applyNotNullEnforcementIfNeeded(
expression = baseExpression,
fieldName = fieldName,
source = source,
target = target
)
}


private fun KSType.isExactlyTarget(): Boolean {
return this.classDeclaration() == targetClassDeclaration
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ class SameTypeConverter : AbstractTypeConverter() {
}

override fun convert(fieldName: String, source: KSType, target: KSType): CodeBlock {
return CodeBlock.of(
fieldName + appendNotNullAssertionOperatorIfNeeded(source, target)
return applyNotNullEnforcementIfNeeded(
expression = CodeBlock.of("%L", fieldName),
fieldName = fieldName,
source = source,
target = target
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,18 @@ abstract class TemporalToDateConverter(
}

override fun convert(fieldName: String, source: KSType, target: KSType): CodeBlock {
val sourceNullable = source.isNullable()
val convertCode = convert(fieldName, if (sourceNullable) "?" else "")
val nc = if (source.isNullable()) "?" else ""
val expression = CodeBlock.of("%L", convert(fieldName, nc))

return CodeBlock.of(
convertCode + appendNotNullAssertionOperatorIfNeeded(source, target)
return applyNotNullEnforcementIfNeeded(
expression = expression,
fieldName = fieldName,
source = source,
target = target
)
}


override val enabledByDefault: Boolean = true

abstract fun convert(fieldName: String, nc: String): String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ abstract class TemporalToTemporalConverter(
}

override fun convert(fieldName: String, source: KSType, target: KSType): CodeBlock {
val sourceNullable = source.isNullable()
val convertCode = convert(fieldName, if (sourceNullable) "?" else "")

return CodeBlock.of(
convertCode + appendNotNullAssertionOperatorIfNeeded(source, target)
val nc = if (source.isNullable()) "?" else ""
val expression = CodeBlock.of("%L", convert(fieldName, nc))

return applyNotNullEnforcementIfNeeded(
expression = expression,
fieldName = fieldName,
source = source,
target = target
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,18 @@ abstract class TemporalToXConverter(
}

override fun convert(fieldName: String, source: KSType, target: KSType): CodeBlock {
val sourceNullable = source.isNullable()
val convertCode = convert(fieldName, if (sourceNullable) "?" else "")

return CodeBlock.of(
convertCode + appendNotNullAssertionOperatorIfNeeded(source, target)
val nc = if (source.isNullable()) "?" else ""
val expression = CodeBlock.of("%L", convert(fieldName, nc))

return applyNotNullEnforcementIfNeeded(
expression = expression,
fieldName = fieldName,
source = source,
target = target
)
}


override val enabledByDefault: Boolean = true

abstract fun convert(fieldName: String, nc: String): String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ class ToAnyConverter : AbstractTypeConverter() {
}

override fun convert(fieldName: String, source: KSType, target: KSType): CodeBlock {
return CodeBlock.of(
fieldName + appendNotNullAssertionOperatorIfNeeded(source, target)
val expression = CodeBlock.of("%L", fieldName)

return applyNotNullEnforcementIfNeeded(
expression = expression,
fieldName = fieldName,
source = source,
target = target
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,18 @@ abstract class XToDateConverter(
}

override fun convert(fieldName: String, source: KSType, target: KSType): CodeBlock {
val sourceNullable = source.isNullable()
val convertCode = convert(fieldName, if (sourceNullable) "?" else "")
val nc = if (source.isNullable()) "?" else ""
val expression = CodeBlock.of("%L", convert(fieldName, nc))

return CodeBlock.of(
convertCode + appendNotNullAssertionOperatorIfNeeded(source, target)
return applyNotNullEnforcementIfNeeded(
expression = expression,
fieldName = fieldName,
source = source,
target = target
)
}


override val enabledByDefault: Boolean = false

abstract fun convert(fieldName: String, nc: String): String
Expand Down
Loading