一键导入
migrate-java-to-kotlin
Migrate a Java package or class to Kotlin using the Kotlin and other coding conventions of this project.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Migrate a Java package or class to Kotlin using the Kotlin and other coding conventions of this project.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | migrate-java-to-kotlin |
| description | Migrate a Java package or class to Kotlin using the Kotlin and other coding conventions of this project. |
Migrate the Java package $ARGUMENTS to Kotlin, following the project's history-preserving two-step commit strategy.
Given the source package $ARGUMENTS:
net.atos. with src/main/java/net/atos/ and dots with slashes.
Example: net.atos.client.bag → src/main/java/net/atos/client/bag/net.atos. prefix with nl.info..
Example: net.atos.client.bag → nl.info.client.bagsrc/main/java/net/atos/ with src/main/kotlin/nl/info/.
Example: src/main/java/net/atos/client/bag/ → src/main/kotlin/nl/info/client/bag/Read every .java file in the source directory (including sub-directories). Note:
src/test/java/ or src/test/kotlin/ that import the old packageCheck whether the converted classes have adequate unit test coverage:
src/test/java/ and src/test/kotlin/ that cover the migrated package.src/test/kotlin/nl/info/<subpath>/ following the conventions of nearby test files.fromValue / companion factory methodsWrite idiomatic Kotlin tests (JUnit 5 + Mockk or the framework already used in the module). Do not add tests for trivial getters or delegating one-liners that provide no value.
./gradlew test
./gradlew itest
Fix any failing tests.
Create the full target directory tree (mirroring all sub-directories found).
For every .java file found, run git mv <old-path> <new-path> changing:
src/main/java/net/atos/ → src/main/kotlin/nl/info/.java → .ktThen commit:
chore: rename $ARGUMENTS Java files to .kt for Kotlin conversion
At this point the .kt files still contain Java source — that is intentional and temporary.
Edit every .kt file in the target directory. Apply these transformations:
a) SPDX header — preserve existing holders/years and, only if INFO.nl is not already mentioned in the SPDX header, add the current year and INFO.nl, for example:
/*
* SPDX-FileCopyrightText: <original holders/years>, <CURRENT_YEAR> INFO.nl
* SPDX-License-Identifier: EUPL-1.2+
*/
b) Package declaration — update to the target package (e.g. nl.info.client.bag).
c) Imports — update any net.atos.* imports to nl.info.*. Remove Java stdlib imports that have Kotlin equivalents.
d) Classes:
public modifier (Kotlin default is public)@ApplicationScoped public class Foo { } → @ApplicationScoped @NoArgConstructor @AllOpen class Foo@Inject constructor pair → single class Foo @Inject constructor(...)private final Type field; → constructor parameter private val field: Typepublic static final String X = "y"; → companion object { const val X = "y" }e) Methods:
public, final modifiers@Override (use override keyword)=) for single-expression methodsOptional<T> → nullable T?; Optional.of(x) / Optional.empty() → x / nullCollections.emptyList() / Collections.emptyMap() → emptyList() / emptyMap().stream().map(this::fn).toList() → .map(::fn)java.util.logging.Logger with the existing pattern, e.g. Logger.getLogger(X.class) → companion object { private val LOG = Logger.getLogger(Foo::class.java.name) }; LOG.fine("v: " + v) → LOG.fine { "v: $v" } (or the existing lambda/Supplier style used in the codebase)f) Model/POJO classes → data class with constructor parameters where all fields are conceptually immutable; plain class with var fields when mutability is needed (e.g. JAX-RS @BeanParam beans).
g) Interfaces — remove public; Java annotations on interface methods translate directly.
h) JAX-RS / MicroProfile REST Client interfaces:
@RegisterRestClient(configKey = "...") etc. work the sameSomeClass.class → SomeClass::class@Produces({X, Y}) → @Produces(X, Y) (vararg, no array literal needed)@Throws(ProcessingException::class) if the original had throws@Suppress("LongParameterList") before funi) Enums — convert to Kotlin enum class; fromValue companion methods → companion object { fun fromValue(...) }.
j) Adapters / small utility classes — convert straightforwardly; implements JsonbAdapter<A, B> → : JsonbAdapter<A, B>.
k) Bean field annotations - prefix bean field annotations with @field: to ensure they apply to the generated field. For example:
convert this:
@QueryParam("fakeFieldName")
to this
@field:QueryParam("fakeFieldName")
l) Use named parameters — when calling methods with multiple parameters, use named arguments for clarity:
// Java: someMethod(x, y, z);
// Kotlin: someMethod(x = x, y = y, z = z)
m) Add extension functions — if the original Java class had static utility methods that operate on instances of a class, consider converting them to Kotlin extension functions for better discoverability and idiomatic usage. For example:
public static NoteDto toDto(Note note) { ... }
public static Note fromDto(NoteDto dto) { ... }
}
could be converted to:
fun Note.toDto(): NoteDto { ... }
fun NoteDto.fromDto(): Note { ... }
}
This allows callers to use the conversion methods in a more natural way:
val note = noteDto.fromDto()
n) Nullability — default to non-null, widen only when a real caller needs it
Java has no compile-time nullability, so every Java parameter/field/return type is a candidate for either T or T? in Kotlin — picking T? everywhere is the easy way out, but it throws away most of the benefit of migrating to Kotlin. Decide per member, not per file:
Find every real call site first, not just the ones in the file being converted. A single grep for Foo.methodName( misses:
import ...Foo.methodName then methodName(x) with no Foo. prefix.list.map(Foo::methodName) or x?.let(Foo::methodName).Grep for the bare method name (grep -rn "methodName(") across src/main and src/test, and also check for Foo::methodName if the function looks like it's ever passed around.
For each parameter, check whether every real call site already holds a non-null value at that point (e.g. it came from a ?.let { }, an !!, an @NotNull-annotated Java field, or a Kotlin-declared non-null type) — or whether at least one call site holds a genuinely optional value (a nullable Kotlin field, a Java field that is documented/annotated as optional, or business data that legitimately doesn't always exist, e.g. Zaak.einddatum for a case that isn't closed yet, TaskInfo.claimTime for an unclaimed task).
?.let(...), !!, or a clear ?: error("...")/exception instead of leaving the utility function nullable "just in case".Watch for the Java-platform-type trap: an unannotated Java field/method accessed from Kotlin is a flexible platform type (Foo!), so the compiler will silently accept passing it to either a nullable or non-null Kotlin parameter — it will NOT flag a mismatch even if the field is null at runtime. This means you cannot rely on "the compiler didn't complain" as proof that tightening a signature is safe. Check the actual data model (@NotNull annotations, the upstream API spec, existing ?./!! usage at other call sites) to decide real-world nullability, not just what compiles.
Same logic applies to return types: a function that only returns null for a genuinely absent value (e.g. a blank/absent optional input) should return T?; a function that unconditionally transforms its (non-null) input should return T.
This is worth extra care specifically because it's easy to get subtly wrong in the unsafe direction: tightening a parameter/return type to non-null when a real call site actually can be null does not fail to compile if the source is a Java platform type — it just turns a null into a NullPointerException at runtime. When in doubt, re-run ./gradlew test and ./gradlew itest after tightening and double check the specific converted class's callers by hand, don't rely on the compiler alone.
Example — a Java-era conversion utility with a real call site that already unwraps before calling, and one that doesn't:
// Before: nullable both ways "just in case", even though the logic is unconditional
fun convertToLocalDate(date: Date?): LocalDate? = date?.let { LocalDate.ofInstant(it.toInstant(), zoneId) }
// After: non-null in, non-null out — callers with an already-nullable source bridge with `?.let`
fun convertToLocalDate(date: Date): LocalDate = LocalDate.ofInstant(date.toInstant(), zoneId)
// call site with a genuinely optional Date field:
val fataledatum = taskInfo.dueDate?.let(DateTimeConverterUtil::convertToLocalDate)
Search for all files that still import the old package:
grep -r "import net\.atos\." src/ --include="*.java" --include="*.kt" -l
Update imports in every found file (Java callers use the same nl.info.* import).
Run:
./gradlew compileKotlin compileJava
A clean compile is not proof that nullability was chosen correctly — see step 6n: Java platform types let nullable and non-null signatures compile equally well, so a successful build here doesn't rule out a call site being tightened past what its data actually guarantees.
Fix any type errors (common: nullable/non-null mismatches, missing @Suppress annotations).
./gradlew spotlessApply detektApply
Fix any remaining Detekt violations — the most common in API interfaces is LongParameterList, which should be suppressed with @Suppress("LongParameterList") since the parameter count is dictated by the external API contract.
./gradlew test
./gradlew itest
Fix any failing tests.
Stage all changes and commit:
chore: convert $ARGUMENTS package to Kotlin
Moves all classes from $ARGUMENTS to <target-package>
and converts Java syntax to idiomatic Kotlin.
git log --oneline --follow -- src/main/kotlin/nl/info/<path>/<MainClass>.kt
The log should show: the conversion commit + the rename commit + the full original Java history.
net.atos.* → nl.info.*; path src/main/java/net/atos/ → src/main/kotlin/nl/info/@NoArgConstructor and @AllOpen from nl.info.zac.util (required on @ApplicationScoped Kotlin classes for Weld proxy support)src/main/kotlin/nl/info/client/pabc/PabcClientService.ktsrc/main/kotlin/nl/info/zac/app/note/converter/NoteConverter.ktsrc/generated/