Share
Embed
【KotlinFest 2026】Kotlin × グラフ理論:Unstable APIで挑む関数レベルAffected Test Selectionライブラリ
Kotlin Fest 2026登壇資料
Haruto K.
November 13, 2026
Transcript
153 slides
-
⾃⼰紹介 加藤 遥⽃ (@HalunoYo) Android Engineer @ BizReach 低レイヤー &
セキュリティ好き ※本発表は所属企業とは関係ありません Haruto Kato 2 -
悩みの紹介 func A() func B() @Test func A() +9 func
A() func C() Haruto Kato @Test func B() @Test func C() 4 -
悩みの紹介 func A() func B() @Test func A() +9 func
A() func C() Haruto Kato @Test func B() @Test func C() 5 -
ノードB,Dの関係性 B A F C Haruto Kato fun B() {
?/ Dを呼ぶ val result = D() } D E 21 -
ノードD,Fの関係性 B A F C Haruto Kato fun D() {
?/ Fを呼ぶ val result = F() } D E 22 -
変更関数ドメインモデル data class ChangedFunction { val fqn: CallableFqn, val moduleName:
ModuleName, … } Haruto Kato 29 -
処理内容 fun B() { val result = D() + }
Haruto Kato ChangedFunction( fqn = “com.hogehoge.B”, moduleName = “core”, ) 30 -
意味解析の不明点 class KotlinLove { fun love(){ return hoge.funcA() } }
funcAとは...? Haruto Kato 41 -
意味解析の不明点 class KotlinLove { fun love(){ return hoge.funcA() } }
io.github.sazanami.hoge.funcA Haruto Kato 42 -
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 -
コンテキスト登録 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 -
コンテキスト登録 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 -
コンテキスト登録 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 -
コンテキスト登録 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 -
コンテキスト登録 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 -
コンテキスト登録 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 -
コンテキスト登録 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 -
KtFile全⾛査 1 2 3 4 5 6 7 8 Haruto
Kato package io.github.kotlin_love import … class KotlinLove { fun love(){ return } } 64 -
KtFile全⾛査 1 2 3 4 5 6 7 8 Haruto
Kato package io.github.kotlin_love import … class KotlinLove { fun love(){ return } } 65 -
KtFile全⾛査 1 2 3 4 5 6 7 8 Haruto
Kato package io.github.kotlin_love import … class KotlinLove { fun love(){ return } } 66 -
KtFile全⾛査 1 2 3 4 5 6 7 8 Haruto
Kato package io.github.kotlin_love import … class KotlinLove { fun love(){ return } } 67 -
KtFile全⾛査 改⾏無しパターン 改⾏有りパターン class KotlinLove { fun love(){ return }
} class KotlinLove { fun love() { return } } Haruto Kato 68 -
PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit
{ return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 71 -
PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit
{ return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 72 -
PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit
{ return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 73 -
PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit
{ return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 74 -
PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit
{ return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 75 -
PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit
{ return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 76 -
PSI Tree化 KtClass KtClassBody class KotlinLove { fun love(): Unit
{ return } } KtNamedFunction KtTypeReference KtBlockExpression KtReturnExpression Haruto Kato 77 -
関数呼び出しの検出 fun processFile(file: KtFile) { file.accept(object : KtTreeVisitorVoid() { override
fun visitCallExpression(expression: KtCallExpression){ processCallExpression(expression) super.visitCallExpression(expression) } } } Haruto Kato 81 -
関数呼び出しの検出 fun processFile(file: KtFile) { file.accept(object : KtTreeVisitorVoid() { override
fun visitCallExpression(expression: KtCallExpression){ processCallExpression(expression) super.visitCallExpression(expression) } } } Haruto Kato 82 -
関数呼び出しの検出 fun processFile(file: KtFile) { file.accept(object : KtTreeVisitorVoid() { override
fun visitCallExpression(expression: KtCallExpression){ processCallExpression(expression) super.visitCallExpression(expression) } } } Haruto Kato 83 -
呼び出し元関数の検出 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 -
呼び出し元関数の検出 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 -
呼び出し元関数の検出 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 -
呼び出し元関数の検出 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 -
呼び出し元関数の検出 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 -
グラフデータの持ち⽅ - NG B map(A to listOf(B,C)) A C ある関数が、どの関数たちを呼んでいるか?
Haruto Kato 100 -
CallGraph実装 class CallGraph { private val edges: MutableMap<Node, MutableSet<Node?> }
fun addEdge(caller: Node, callee: Node) { … } … Haruto Kato 102 -
グラフ構築 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 -
関数呼び出しの検出 fun processFile(file: KtFile) { file.accept(object : KtTreeVisitorVoid() { override
fun visitCallExpression(expression: KtCallExpression){ processCallExpression(expression) super.visitCallExpression(expression) } } } Haruto Kato 104 -
グラフ構築 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 -
グラフ構築 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 -
グラフ構築 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 -
グラフ構築 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 -
グラフ構築 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 -
影響範囲⾛査 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 -
影響範囲⾛査 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 -
影響範囲⾛査 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 -
影響範囲⾛査 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 -
影響範囲⾛査 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 -
影響範囲⾛査 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 -
影響範囲⾛査 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 -
影響範囲⾛査 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 -
影響範囲⾛査 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 -
影響範囲⾛査 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 -
影響範囲⾛査 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 -
影響範囲⾛査 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 -
影響範囲⾛査 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 -
技術の破綻(Observerパターン) fun emitter() = dispatch(EVENT_A) fun collector() = runBlocking {
EVENT_A().collect { … } } Haruto Kato 144