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

【KotlinFest 2026】Kotlin × グラフ理論:Unstable APIで挑む...

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →
Haruto K.
November 13, 2026
25

【KotlinFest 2026】Kotlin × グラフ理論:Unstable APIで挑む関数レベルAffected Test Selectionライブラリ

Kotlin Fest 2026登壇資料

Haruto K.

November 13, 2026

Transcript

153 slides
  1. ⾃⼰紹介 加藤 遥⽃ (@HalunoYo) Android Engineer @ BizReach 低レイヤー &

    セキュリティ好き ※本発表は所属企業とは関係ありません Haruto Kato 2
  2. 悩みの紹介 func A() func B() @Test func A() +9 func

    A() func C() Haruto Kato @Test func B() @Test func C() 4
  3. 悩みの紹介 func A() func B() @Test func A() +9 func

    A() func C() Haruto Kato @Test func B() @Test func C() 5
  4. ノードB,Dの関係性 B A F C Haruto Kato fun B() {

    ?/ Dを呼ぶ val result = D() } D E 21
  5. ノードD,Fの関係性 B A F C Haruto Kato fun D() {

    ?/ Fを呼ぶ val result = F() } D E 22
  6. 処理内容 fun B() { val result = D() + }

    Haruto Kato ChangedFunction( fqn = “com.hogehoge.B”, moduleName = “core”, ) 30
  7. build.gradle.kts dependencies { implementation("kotlin:analysis-api-for-ide:$ktVersion") { isTransitive = false } //

    他にも⾊々インポート } implementation("kotlin:symbol-light-classes-for-ide:$ktVersion"){ isTransitive = false } Haruto Kato 44
  8. コンテキスト登録 fun buildStandaloneAnalysisSession() { val session = buildStandaloneAnalysisAPISession(disposable) { for

    (moduleName in sortedModules) { … val module = buildKtSourceModule { this.moduleName = moduleName sourceRoots.forEach { addSourceRoot(it) } for (dep in allDependencies) { … addRegularDependency(dep) } } addModule(module) } } } Haruto Kato 53
  9. コンテキスト登録 fun buildStandaloneAnalysisSession() { val session = buildStandaloneAnalysisAPISession(disposable) { for

    (moduleName in sortedModules) { … val module = buildKtSourceModule { this.moduleName = moduleName sourceRoots.forEach { addSourceRoot(it) } for (dep in allDependencies) { … addRegularDependency(dep) } } addModule(module) } } } Haruto Kato 54
  10. コンテキスト登録 fun buildStandaloneAnalysisSession() { val session = buildStandaloneAnalysisAPISession(disposable) { for

    (moduleName in sortedModules) { … val module = buildKtSourceModule { this.moduleName = moduleName sourceRoots.forEach { addSourceRoot(it) } for (dep in allDependencies) { … addRegularDependency(dep) } } addModule(module) } } } Haruto Kato 56
  11. コンテキスト登録 fun buildStandaloneAnalysisSession() { val session = buildStandaloneAnalysisAPISession(disposable) { for

    (moduleName in sortedModules) { … val module = buildKtSourceModule { this.moduleName = moduleName sourceRoots.forEach { addSourceRoot(it) } for (dep in allDependencies) { … addRegularDependency(dep) } } addModule(module) } } } Haruto Kato 57
  12. コンテキスト登録 fun buildStandaloneAnalysisSession() { val session = buildStandaloneAnalysisAPISession(disposable) { for

    (moduleName in sortedModules) { … val module = buildKtSourceModule { this.moduleName = moduleName sourceRoots.forEach { addSourceRoot(it) } for (dep in allDependencies) { … addRegularDependency(dep) } } addModule(module) } } } Haruto Kato 58
  13. コンテキスト登録 fun buildStandaloneAnalysisSession() { val session = buildStandaloneAnalysisAPISession(disposable) { for

    (moduleName in sortedModules) { … val module = buildKtSourceModule { this.moduleName = moduleName sourceRoots.forEach { addSourceRoot(it) } for (dep in allDependencies) { … addRegularDependency(dep) } } addModule(module) } } } Haruto Kato 59
  14. コンテキスト登録 fun buildStandaloneAnalysisSession() { val session = buildStandaloneAnalysisAPISession(disposable) { for

    (moduleName in sortedModules) { … val module = buildKtSourceModule { this.moduleName = moduleName sourceRoots.forEach { addSourceRoot(it) } for (dep in allDependencies) { … addRegularDependency(dep) } } addModule(module) } } } Haruto Kato 60
  15. KtFile全⾛査 1 2 3 4 5 6 7 8 Haruto

    Kato package io.github.kotlin_love import … class KotlinLove { fun love(){ return } } 64
  16. KtFile全⾛査 1 2 3 4 5 6 7 8 Haruto

    Kato package io.github.kotlin_love import … class KotlinLove { fun love(){ return } } 65
  17. KtFile全⾛査 1 2 3 4 5 6 7 8 Haruto

    Kato package io.github.kotlin_love import … class KotlinLove { fun love(){ return } } 66
  18. KtFile全⾛査 1 2 3 4 5 6 7 8 Haruto

    Kato package io.github.kotlin_love import … class KotlinLove { fun love(){ return } } 67
  19. PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit

    { return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 71
  20. PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit

    { return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 72
  21. PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit

    { return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 73
  22. PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit

    { return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 74
  23. PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit

    { return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 75
  24. PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit

    { return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 76
  25. PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit

    { return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 77
  26. 関数呼び出しの検出 fun processFile(file: KtFile) { file.accept(object : KtTreeVisitorVoid() { override

    fun visitCallExpression(expression: KtCallExpression){ processCallExpression(expression) super.visitCallExpression(expression) } } } Haruto Kato 81
  27. 関数呼び出しの検出 fun processFile(file: KtFile) { file.accept(object : KtTreeVisitorVoid() { override

    fun visitCallExpression(expression: KtCallExpression){ processCallExpression(expression) super.visitCallExpression(expression) } } } Haruto Kato 82
  28. 関数呼び出しの検出 fun processFile(file: KtFile) { file.accept(object : KtTreeVisitorVoid() { override

    fun visitCallExpression(expression: KtCallExpression){ processCallExpression(expression) super.visitCallExpression(expression) } } } Haruto Kato 83
  29. 呼び出し元関数の検出 fun resolveOwnerNode(expression: KtExpression) { var current: PsiElement = expression.parent

    while (current ?= null) { when (current) { is KtNamedFunction ?> { … return current.fqName } … } current = current.parent } } Haruto Kato 89
  30. 呼び出し元関数の検出 fun resolveOwnerNode(expression: KtExpression) { var current: PsiElement = expression.parent

    while (current ?= null) { when (current) { is KtNamedFunction ?> { … return current.fqName } … } current = current.parent } } Haruto Kato 90
  31. 呼び出し元関数の検出 fun resolveOwnerNode(expression: KtExpression) { var current: PsiElement = expression.parent

    while (current ?= null) { when (current) { is KtNamedFunction ?> { … return current.fqName } … } current = current.parent } } Haruto Kato 91
  32. 呼び出し元関数の検出 fun resolveOwnerNode(expression: KtExpression) { var current: PsiElement = expression.parent

    while (current ?= null) { when (current) { is KtNamedFunction ?> { … return current.fqName } … } current = current.parent } } Haruto Kato 92
  33. 呼び出し元関数の検出 fun resolveOwnerNode(expression: KtExpression) { var current: PsiElement = expression.parent

    while (current ?= null) { when (current) { is KtNamedFunction ?> { … return current.fqName } … } current = current.parent } } Haruto Kato 93
  34. CallGraph実装 class CallGraph { private val edges: MutableMap<Node, MutableSet<Node?> }

    fun addEdge(caller: Node, callee: Node) { … } … Haruto Kato 102
  35. グラフ構築 fun processCallExpression(expression: KtExpression) { val caller = resolveOwnerNode(expression) ?:

    return analyze(expression) { val call = expression.resolveToCall() val symbol = call?.singleFunctionCallOrNull()?.symbol symbol?.let { val callee = Node(symbol) graph.addEdge(caller, callee) } } } Haruto Kato 103
  36. 関数呼び出しの検出 fun processFile(file: KtFile) { file.accept(object : KtTreeVisitorVoid() { override

    fun visitCallExpression(expression: KtCallExpression){ processCallExpression(expression) super.visitCallExpression(expression) } } } Haruto Kato 104
  37. グラフ構築 fun processCallExpression(expression: KtExpression) { val caller = resolveOwnerNode(expression) ?:

    return analyze(expression) { val call = expression.resolveToCall() val symbol = call?.singleFunctionCallOrNull()?.symbol symbol?.let { val callee = Node(symbol) graph.addEdge(caller, callee) } } } Haruto Kato 105
  38. グラフ構築 fun processCallExpression(expression: KtExpression) { val caller = resolveOwnerNode(expression) ?:

    return analyze(expression) { val call = expression.resolveToCall() val symbol = call?.singleFunctionCallOrNull()?.symbol symbol?.let { val callee = Node(symbol) graph.addEdge(caller, callee) } } } Haruto Kato 106
  39. グラフ構築 fun processCallExpression(expression: KtExpression) { val caller = resolveOwnerNode(expression) ?:

    return analyze(expression) { val call = expression.resolveToCall() val symbol = call?.singleFunctionCallOrNull()?.symbol symbol?.let { val callee = Node(symbol) graph.addEdge(caller, callee) } } } Haruto Kato 107
  40. グラフ構築 fun processCallExpression(expression: KtExpression) { val caller = resolveOwnerNode(expression) ?:

    return analyze(expression) { val call = expression.resolveToCall() val symbol = call?.singleFunctionCallOrNull()?.symbol symbol?.let { val callee = Node(symbol) graph.addEdge(caller, callee) } } } Haruto Kato 108
  41. グラフ構築 fun processCallExpression(expression: KtExpression) { val caller = resolveOwnerNode(expression) ?:

    return analyze(expression) { val call = expression.resolveToCall() val symbol = call?.singleFunctionCallOrNull()?.symbol symbol?.let { val callee = Node(symbol) graph.addEdge(caller, callee) } } } Haruto Kato 110
  42. 影響範囲⾛査 fun findAffectedTests(changedFunctions: Set<ChangedFunction>) { val affectedTests = mutableSetOf<Node>() val

    seen = mutableSetOf<Node>() val queue = ArrayDeque(changedFunctions) while (queue.isNotEmpty()) { val callee = queue.removeFirst() if (callee in seen) continue seen.add(callee) val callers = graph.getCallersByFqn(callee.fqn) for (caller in callers) { if (caller.isTest) { affectedTests.add(caller.fqn) } queue.add(caller) } } return affectedTests } Haruto Kato 125
  43. 影響範囲⾛査 fun findAffectedTests(changedFunctions: Set<ChangedFunction>) { val affectedTests = mutableSetOf<Node>() val

    seen = mutableSetOf<Node>() val queue = ArrayDeque(changedFunctions) while (queue.isNotEmpty()) { val callee = queue.removeFirst() if (callee in seen) continue seen.add(callee) val callers = graph.getCallersByFqn(callee.fqn) for (caller in callers) { if (caller.isTest) { affectedTests.add(caller.fqn) } queue.add(caller) } } return affectedTests } Haruto Kato 126
  44. 影響範囲⾛査 fun findAffectedTests(changedFunctions: Set<ChangedFunction>) { val affectedTests = mutableSetOf<Node>() val

    seen = mutableSetOf<Node>() val queue = ArrayDeque(changedFunctions) while (queue.isNotEmpty()) { val callee = queue.removeFirst() if (callee in seen) continue seen.add(callee) val callers = graph.getCallersByFqn(callee.fqn) for (caller in callers) { if (caller.isTest) { affectedTests.add(caller.fqn) } queue.add(caller) } } return affectedTests } Haruto Kato 127
  45. 影響範囲⾛査 fun findAffectedTests(changedFunctions: Set<ChangedFunction>) { val affectedTests = mutableSetOf<Node>() val

    seen = mutableSetOf<Node>() val queue = ArrayDeque(changedFunctions) while (queue.isNotEmpty()) { val callee = queue.removeFirst() if (callee in seen) continue seen.add(callee) val callers = graph.getCallersByFqn(callee.fqn) for (caller in callers) { if (caller.isTest) { affectedTests.add(caller.fqn) } queue.add(caller) } } return affectedTests } Haruto Kato 128
  46. 影響範囲⾛査 fun findAffectedTests(changedFunctions: Set<ChangedFunction>) { val affectedTests = mutableSetOf<Node>() val

    seen = mutableSetOf<Node>() val queue = ArrayDeque(changedFunctions) while (queue.isNotEmpty()) { val callee = queue.removeFirst() if (callee in seen) continue seen.add(callee) val callers = graph.getCallersByFqn(callee.fqn) for (caller in callers) { if (caller.isTest) { affectedTests.add(caller.fqn) } queue.add(caller) } } return affectedTests } Haruto Kato 129
  47. 影響範囲⾛査 fun findAffectedTests(changedFunctions: Set<ChangedFunction>) { val affectedTests = mutableSetOf<Node>() val

    seen = mutableSetOf<Node>() val queue = ArrayDeque(changedFunctions) while (queue.isNotEmpty()) { val callee = queue.removeFirst() if (callee in seen) continue seen.add(callee) val callers = graph.getCallersByFqn(callee.fqn) for (caller in callers) { if (caller.isTest) { affectedTests.add(caller.fqn) } queue.add(caller) } } return affectedTests } Haruto Kato 130
  48. 影響範囲⾛査 fun findAffectedTests(changedFunctions: Set<ChangedFunction>) { val affectedTests = mutableSetOf<Node>() val

    seen = mutableSetOf<Node>() val queue = ArrayDeque(changedFunctions) while (queue.isNotEmpty()) { val callee = queue.removeFirst() if (callee in seen) continue seen.add(callee) val callers = graph.getCallersByFqn(callee.fqn) for (caller in callers) { if (caller.isTest) { affectedTests.add(caller.fqn) } queue.add(caller) } } return affectedTests } Haruto Kato 131
  49. 影響範囲⾛査 fun findAffectedTests(changedFunctions: Set<ChangedFunction>) { val affectedTests = mutableSetOf<Node>() val

    seen = mutableSetOf<Node>() val queue = ArrayDeque(changedFunctions) while (queue.isNotEmpty()) { val callee = queue.removeFirst() if (callee in seen) continue seen.add(callee) val callers = graph.getCallersByFqn(callee.fqn) for (caller in callers) { if (caller.isTest) { affectedTests.add(caller.fqn) } queue.add(caller) } } return affectedTests } Haruto Kato 132
  50. 影響範囲⾛査 fun findAffectedTests(changedFunctions: Set<ChangedFunction>) { val affectedTests = mutableSetOf<Node>() val

    seen = mutableSetOf<Node>() val queue = ArrayDeque(changedFunctions) while (queue.isNotEmpty()) { val callee = queue.removeFirst() if (callee in seen) continue seen.add(callee) val callers = graph.getCallersByFqn(callee.fqn) for (caller in callers) { if (caller.isTest) { affectedTests.add(caller.fqn) } queue.add(caller) } } return affectedTests } Haruto Kato 133
  51. 影響範囲⾛査 fun findAffectedTests(changedFunctions: Set<ChangedFunction>) { val affectedTests = mutableSetOf<Node>() val

    seen = mutableSetOf<Node>() val queue = ArrayDeque(changedFunctions) while (queue.isNotEmpty()) { val callee = queue.removeFirst() if (callee in seen) continue seen.add(callee) val callers = graph.getCallersByFqn(callee.fqn) for (caller in callers) { if (caller.isTest) { affectedTests.add(caller.fqn) } queue.add(caller) } } return affectedTests } Haruto Kato 134
  52. 影響範囲⾛査 fun findAffectedTests(changedFunctions: Set<ChangedFunction>) { val affectedTests = mutableSetOf<Node>() val

    seen = mutableSetOf<Node>() val queue = ArrayDeque(changedFunctions) while (queue.isNotEmpty()) { val callee = queue.removeFirst() if (callee in seen) continue seen.add(callee) val callers = graph.getCallersByFqn(callee.fqn) for (caller in callers) { if (caller.isTest) { affectedTests.add(caller.fqn) } queue.add(caller) } } return affectedTests } Haruto Kato 135
  53. 影響範囲⾛査 fun findAffectedTests(changedFunctions: Set<ChangedFunction>) { val affectedTests = mutableSetOf<Node>() val

    seen = mutableSetOf<Node>() val queue = ArrayDeque(changedFunctions) while (queue.isNotEmpty()) { val callee = queue.removeFirst() if (callee in seen) continue seen.add(callee) val callers = graph.getCallersByFqn(callee.fqn) for (caller in callers) { if (caller.isTest) { affectedTests.add(caller.fqn) } queue.add(caller) } } return affectedTests } Haruto Kato 136
  54. 影響範囲⾛査 fun findAffectedTests(changedFunctions: Set<ChangedFunction>) { val affectedTests = mutableSetOf<Node>() val

    seen = mutableSetOf<Node>() val queue = ArrayDeque(changedFunctions) while (queue.isNotEmpty()) { val callee = queue.removeFirst() if (callee in seen) continue seen.add(callee) val callers = graph.getCallersByFqn(callee.fqn) for (caller in callers) { if (caller.isTest) { affectedTests.add(caller.fqn) } queue.add(caller) } } return affectedTests } Haruto Kato 137