Upgrade to Pro — share decks privately, control downloads, hide ads and more …

デッドコード消せてますか?構文解析とGradleプラグイン開発で始めるコードベース改善

Avatar for Tasuku Nakagawa Tasuku Nakagawa
October 30, 2025
5

 デッドコード消せてますか?構文解析とGradleプラグイン開発で始めるコードベース改善

デッドコードはコードの保守性を低下させる要因の一つですが、新規開発が優先されることが多く、結果としてコードベースの健全性が損なわれることがあります。
私たちのプロダクトでは、開発生産性向上のためにフィーチャーフラグを活用しています。
しかし、リリース後に不要となったフィーチャーフラグがデッドコードとして残る問題に直面しました。
この問題を解決するために、kotlin-compiler-embeddableを用いた構文解析とGradleプラグイン開発によるデッドコード削除の自動化を試みました。

このセッションでは、それぞれの実装方法について言及しつつ、フィーチャーフラグに起因するデッドコードの削除を自動化した経験を共有します。
これらのテクニックはデッドコード削除以外にも応用可能であり、セッション参加者の皆様のアイデア実現の一助となれば幸いです。

Avatar for Tasuku Nakagawa

Tasuku Nakagawa

October 30, 2025
Tweet

Transcript

  1. fun main() { val isReleased = isFeatureReleased("CALC_FEE") if (isReleased) {

    calcFee(100) } else { oldCalcFee(100) } } fun calcFee(value: Int) = value * 0.95 fun oldCalcFee(value: Int) = value * 0.9
  2. fun main() { val isReleased = isFeatureReleased("CALC_FEE") if (isReleased) {

    calcFee(100) } else { oldCalcFee(100) } } fun calcFee(value: Int) = value * 0.95 fun oldCalcFee(value: Int) = value * 0.9
  3. fun main() { val isReleased = isFeatureReleased("CALC_FEE") if (isReleased) {

    calcFee(100) } else { oldCalcFee(100) } } fun calcFee(value: Int) = value * 0.95 fun oldCalcFee(value: Int) = value * 0.9
  4. fun main() { val isReleased = isFeatureReleased("CALC_FEE") if (isReleased) {

    calcFee(100) } else { oldCalcFee(100) } } fun calcFee(value: Int) = value * 0.95 fun oldCalcFee(value: Int) = value * 0.9
  5. fun main() { val isReleased = isFeatureReleased("CALC_FEE") if (isReleased) {

    calcFee(100) } else { oldCalcFee(100) } } fun calcFee(value: Int) = value * 0.95 fun oldCalcFee(value: Int) = value * 0.9
  6. fun main() { val isReleased = isFeatureReleased("CALC_FEE") if (isReleased) {

    calcFee(100) } else { oldCalcFee(100) } } fun calcFee(value: Int) = value * 0.95 fun oldCalcFee(value: Int) = value * 0.9
  7. fun main() { val isReleased = isFeatureReleased("CALC_FEE") if (isReleased) {

    calcFee(100) } else { oldCalcFee(100) } } fun calcFee(value: Int) = value * 0.95 fun oldCalcFee(value: Int) = value * 0.9
  8. fun main() { @Remove val isReleased = isFeatureReleased("CALC_FEE") @RemoveElse if

    (isReleased) { calcFee(100) } else { oldCalcFee(100) } } fun calcFee(value: Int) = value * 0.95 @Remove fun oldCalcFee(value: Int) = value * 0.9
  9. if (isReleased) { calcFee(100) } else { oldCalcFee(100) } If

    statement Condition Then clause Else clause Expression isReleased Expression Function call calcFee args: 100 Function call oldCalcFee args: 100 Expression
  10. plugins { kotlin("jvm") version "2.2.1" } dependencies { implementation( mapOf(

    "group" to "org.jetbrains.kotlin", "name" to "kotlin-compiler-embeddable", ) ) }
  11. fun main() { // 構文解析用のファクトリを作る val disposable = Disposer.newDisposable() val

    environment = KotlinCoreEnvironment.createForProduction( disposable, CompilerConfiguration(), EnvironmentConfigFiles.JVM_CONFIG_FILES ) val project = environment.project val factory = KtPsiFactory(project) val kotlinFilePath = Path("/path/to/kotlin/file") // 構文解析を行う val ktFile = factory.createFile(kotlinFilePath.readText()) }
  12. open class KtFile(…) : … { … override fun <R,

    D> accept( visitor: KtVisitor<R, D>, data: D ): R { return visitor.visitKtFile(this, data) } … }
  13. class RemoveTargetVisitor : KtTreeVisitorVoid() { val elements: MutableList<KtElement> = mutableListOf()

    override fun visitNamedFunction(function: KtNamedFunction) { if (element.isAnnotated()) { elements.add(function) } else { return super.visitNamedFunction(function) } } } private fun KtAnnotated.isAnnotated(): Boolean = …
  14. fun calcFee(value: Int) = value * 0.95 @Remove fun oldCalcFee(value:

    Int) = value * 0.9 Root Function declaration calcFee value: Int Body Function declaration oldCalcFee value: Int Body Remove
  15. Root Function declaration calcFee value: Int Body Function declaration oldCalcFee

    value: Int Body Remove Visitor elements = [oldCalcFee]
  16. val sourceCode: String = … … val elements = …

    elements.forEach { sourceCode.removeRange(it.startOffset, it.endOffset) }
  17. fun main() { @Remove val isReleased = false @RemoveElse if

    (isReleased) { calcFee(100) } else { oldCalcFee(100) } } fun calcFee(value: Int) = value * 0.95 @Remove fun oldCalcFee(value: Int) = value * 0.9
  18. class RemoveFeatureFlagTest { @Test @Disabled fun removeFeatureFlag() { val files

    = … files.forEach { file -> val elements = … // Remove elements from file } } }
  19. abstract class RemoveFeatureFlagTask : DefaultTask() { @get:Input abstract val input:

    Property<String> init { group = "group name" description = "description" } @TaskAction fun removeFeatureFlag() { … } }
  20. class FeatureFlagRemoverPlugin : Plugin<Project> { override fun apply(project: Project) {

    project.tasks.register( "removeFeatureFlag", RemoveFeatureFlagTask::class.java ) } }
  21. class RemoveFeatureFlagTest { @Test fun removeFeatureFlag() { val files =

    … files.forEach { file -> val elements = … // Remove elements from file } } } ./gradlew test --tests RemoveFeatureFlagTest