Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Task (Tasuku Nakagawa) 株式会社マネーフォワード 大阪開発拠点 バックエンドエンジニア 兼EM見習い X (Old Twitter): getupmax GitHub: T45K

Slide 3

Slide 3 text

デッドコード

Slide 4

Slide 4 text

fun hoge(): String { return "hoge" // Cannot reach here! throw RuntimeException("fuge") }

Slide 5

Slide 5 text

絶対に到達し得ないコード

Slide 6

Slide 6 text

放置すると ● コードベースの肥大化 ● 誤ったコード理解の誘発 ● etc.

Slide 7

Slide 7 text

消しましょう

Slide 8

Slide 8 text

これは?

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

フィーチャーフラグ

Slide 11

Slide 11 text

フィーチャーフラグ ● システムの動作を動的に変更する仕組み ● 単純な if-else 文によって実装できる ● トランクベース開発には必須 https://martinfowler.com/articles/feature-toggles.html

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

リリース後はデッドコード

Slide 14

Slide 14 text

消しましょう

Slide 15

Slide 15 text

できる?

Slide 16

Slide 16 text

スケジュール ビ ジ ネ ス 要 求 デグレ 費 用 対 効 果 修正箇所の特定

Slide 17

Slide 17 text

削除を自動化したい!

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

fun main() { calcFee(100) } fun calcFee(value: Int) = value * 0.95

Slide 20

Slide 20 text

どうやって削除対象の コードを特定するか

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

完全な自動化は難しそう

Slide 25

Slide 25 text

実装時に明示的に指定する

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

- コードを書くときに 消されることを意識できる - 事故を防ぐ

Slide 29

Slide 29 text

アノテーションがついている コードを特定したい

Slide 30

Slide 30 text

構文解析

Slide 31

Slide 31 text

構文解析 ● ソースコードを構造化された形式へ変換する ● (抽象)構文木という木構造に変換される ● コンパイルの過程で実行される

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Kotlinコンパイラを ライブラリとして使いたい

Slide 34

Slide 34 text

Kotlin Compiler Embeddable

Slide 35

Slide 35 text

plugins { kotlin("jvm") version "2.2.1" } dependencies { implementation(kotlin("compiler-embeddable")) }

Slide 36

Slide 36 text

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()) }

Slide 37

Slide 37 text

open class KtFile(…) : … { … override fun accept( visitor: KtVisitor, data: D ): R { return visitor.visitKtFile(this, data) } … }

Slide 38

Slide 38 text

Visitorパターン

Slide 39

Slide 39 text

Visitorパターン ● オブジェクト構造と、それに対する操作を 切り離すデザインパターン ● 構文木がオブジェクト構造、 対象のコードの特定と保存が操作

Slide 40

Slide 40 text

class RemoveTargetVisitor : KtTreeVisitorVoid() { val elements: MutableList = mutableListOf() override fun visitNamedFunction(function: KtNamedFunction) { if (element.isAnnotated()) { elements.add(function) } else { return super.visitNamedFunction(function) } } } private fun KtAnnotated.isAnnotated(): Boolean = …

Slide 41

Slide 41 text

fun calcFee(value: Int) = value * 0.95 @Remove fun oldCalcFee(value: Int) = value * 0.9

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Root Function declaration calcFee value: Int Body Function declaration oldCalcFee value: Int Body Remove Visitor elements = []

Slide 44

Slide 44 text

Root Function declaration calcFee value: Int Body Function declaration oldCalcFee value: Int Body Remove Visitor elements = []

Slide 45

Slide 45 text

Root Function declaration calcFee value: Int Body Function declaration oldCalcFee value: Int Body Remove Visitor elements = []

Slide 46

Slide 46 text

Root Function declaration calcFee value: Int Body Function declaration oldCalcFee value: Int Body Remove Visitor elements = []

Slide 47

Slide 47 text

Root Function declaration calcFee value: Int Body Function declaration oldCalcFee value: Int Body Remove Visitor elements = [oldCalcFee]

Slide 48

Slide 48 text

集めたelementsを ソースコードから取り除く

Slide 49

Slide 49 text

val sourceCode: String = … … val elements = … elements.forEach { sourceCode.removeRange(it.startOffset, it.endOffset) }

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

fun main() { calcFee(100) } fun calcFee(value: Int) = value * 0.95

Slide 52

Slide 52 text

第一部完

Slide 53

Slide 53 text

課題

Slide 54

Slide 54 text

実行環境

Slide 55

Slide 55 text

元々テストコードとして 書いていた

Slide 56

Slide 56 text

class RemoveFeatureFlagTest { @Test @Disabled fun removeFeatureFlag() { val files = … files.forEach { file -> val elements = … // Remove elements from file } } }

Slide 57

Slide 57 text

● 可搬性が低い ● シュッと実行できない (@Disabledをアンコメントして...)

Slide 58

Slide 58 text

Gradleプラグイン

Slide 59

Slide 59 text

Gradleプラグイン ● Taskとして定義されたロジックを実行できる ● コマンド一発で実行できる ● 他のGradleプロジェクトでも使いまわせる https://docs.gradle.org/current/userguide/plugins.html

Slide 60

Slide 60 text

abstract class RemoveFeatureFlagTask : DefaultTask() { @get:Input abstract val input: Property init { group = "group name" description = "description" } @TaskAction fun removeFeatureFlag() { // フィーチャーフラグを消す一連の処理 } }

Slide 61

Slide 61 text

class FeatureFlagRemoverPlugin : Plugin { override fun apply(project: Project) { project.tasks.register( "removeFeatureFlag", RemoveFeatureFlagTask::class.java ) } }

Slide 62

Slide 62 text

plugins { id("feature-flag-remover") version "1.0.0" }

Slide 63

Slide 63 text

class RemoveFeatureFlagTest { @Test fun removeFeatureFlag() { val files = … files.forEach { file -> val elements = … // Remove elements from file } } } ./gradlew test --tests RemoveFeatureFlagTest

Slide 64

Slide 64 text

./gradlew removeFeatureFlag feature_name

Slide 65

Slide 65 text

第二部完

Slide 66

Slide 66 text

● 他のプロジェクトに適用したり ● 非バックエンドメンバーでも 作業できるようになったり ● AI Agentのワークフローに 組み込んだり

Slide 67

Slide 67 text

俺たちの旅はまだまだこれからだ... ● Kotlin Compiler Embeddingのセットアップが Deprecatedになってた ● Gradle Pluginのテスト方法は色々試したい ● どちらの技術も完全に理解できてない

Slide 68

Slide 68 text

まとめ

Slide 69

Slide 69 text

フィーチャーフラグによる デッドコードの除去を 自動化した - 構文解析で特定 - Gradleプラグインで実行

Slide 70

Slide 70 text

いろんな用途で使えそう - 他のリファクタリングを 自動化したり - Linterを実装したり

Slide 71

Slide 71 text

皆様のアイデアを実現する 一助になれば!

Slide 72

Slide 72 text

No content

Slide 73

Slide 73 text

成果物リンク https://github.com/T45K/feature-flag-remover https://github.com/T45K/feature-flag-remover-plugin