Slide 1

Slide 1 text

Java→Kotlinの移行率を算出する コマンドラインツールを Kotlinで実装してみた話 Yu Mitsuhori

Slide 2

Slide 2 text

自己紹介 三堀 裕 Android Developer 女性向けのヘルスケアアプリを担当してます。  Twitter: @1013Youmeee  GitHub: https://github.com/youmitsu  Qiita: https://qiita.com/youmeee

Slide 3

Slide 3 text

Question 皆さんKotlinで何を作りますか?

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

コマンドラインツールも作れます

Slide 8

Slide 8 text

Java→Kotlinの移行率を算出する コマンドラインツール を Kotlin で 作ってみました

Slide 9

Slide 9 text

KotlinReplaceSupporter

Slide 10

Slide 10 text

作ったもの 計算対象となるファイ ル一覧を出力 Projectルートで、 コマンド叩く 移行率の表示

Slide 11

Slide 11 text

本日のアジェンダ 1. 経緯 2. デモ 3. 実装方法 a. 環境構築 b. 実装 c. jarの実行 4. まとめ

Slide 12

Slide 12 text

経緯 ・会社の評価の際にKotlin→Javaのリプレース率を出さなければいけな い時があった ・一つ一つ手で計算するのもあれだし、Kotlinで書けるなら勉強も兼ねて やってみよう!どうせならツール化もしてみよう! (シェルとか書けば一発な気もしますがそこはスルーでお願いしま す。。)

Slide 13

Slide 13 text

デモ

Slide 14

Slide 14 text

開発環境 ・Mac Book Pro Sierra 10.12.6 ・IntelliJ IDEA Community 2017.3 ・Kotlin 1.2.10

Slide 15

Slide 15 text

環境準備 1. File > New > Projectを押下 2. 左メニューからGradleを選択後、Kotlinのチェックを入れる 3. そのまま必要項目を入力してプロジェクトを作成する ↑2の選択画面 3の 作成されるプロジェクト →

Slide 16

Slide 16 text

おおまかな処理フロー 1. 言語情報を保持するクラスを定義し、初期化する 2. 計算対象に入れたくないファイルをignoreするための Readerクラスを定義し、ロードする 3. ディレクトリの探索 4. 移行率の計算、ログの出力

Slide 17

Slide 17 text

実装①:言語情報を保持するオブジェクトの作成 /** * 言語の情報を保持するクラス * */ data class Language( val name: String, //言語名 val extension: String //拡張子 ){ override fun toString() = name }

Slide 18

Slide 18 text

各言語のファイルリスト、移行元/先情報のクラス /** * 言語のlistを保持するクラス */ class SourceList( val language: Language, val isReplaceTarget: Boolean, val list: MutableList = mutableListOf()) { fun add(file: File){ list.add(file) } }

Slide 19

Slide 19 text

Main.kt(SourceListの初期化) val javaList = SourceList(Language("Java", "java"), false) val kotlinList = SourceList(Language("Kotlin", "kt"), true) SourceListを初期化する 移行元がfalse, 移行先がtrue

Slide 20

Slide 20 text

実装②:計算対象に入れたくないファイルを無視する ・Androidだと自動生成されたファイル (R, DataBindingのファイルなど)を無視したい時あると思います。 ・.krsignoreというファイル名でProjectルートからの相対パスで記述する ことで、計算対象から外すように実装する

Slide 21

Slide 21 text

ignoreファイルを読み取るためのReaderクラスの作成 class IgnoreFileReader(ignoreFilePath: String, private val cd: String) { private var fileReader: FileReader? = null var files: List = mutableListOf() init { try { fileReader = FileReader(ignoreFilePath) } catch(e: FileNotFoundException) { } } fun read() { fileReader?.let { val filesStr = it.readLines() files = filesStr.map { File("$cd/$it") } } } }

Slide 22

Slide 22 text

Main.kt(ignoreファイル読み込み) val homeDir = System.getProperty("user.home") ?: "" val cd = File(".").absoluteFile.parent val ignoreFileReader = IgnoreFileReader("$homeDir/.krsignore", cd) ignoreFileReader.read() .krsignoreファイルを参照して、プロジェクトのルートディレクトリ から初期化したFileオブジェクトを渡す

Slide 23

Slide 23 text

実装③:ディレクトリ探索用のクラス class SearchHandler( private val path: String, private val langList: Array, private val ignoreDirList: List) { lateinit var result: Result fun execute() { var sizeAll = 0 val rootDir = File(path) rootDir.walkTopDown() .filter { isNotIgnoreFile(it) }.forEach { file -> langList.map { langList -> if(isTargetFile(file, langList.language.extension)){ langList.add(file) println(file) sizeAll++ } } } result = Result(langList, sizeAll) } private fun isTargetFile(file: File, extension: String): Boolean = file.extension == extension /** * 対象外ファイルを無視する */ private fun isNotIgnoreFile(file: File): Boolean = ignoreDirList.none { file.startsWith(it) } }

Slide 24

Slide 24 text

ファイルを再帰的に探索する File.walkTopDown() Fileクラスに拡張関数として実装されている。 FileTreeWalkというクラスが返却される。これがSequenceイン ターフェースを実装しているためforEachで各Fileオブジェクトを ループで取得できる https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-fil e/walk-top-down.html

Slide 25

Slide 25 text

Main.kt(探索処理及び結果表示) val handler = SearchHandler(cd, arrayOf(javaList, kotlinList), ignoreFileReader.files) handler.execute() val logger = ResultLogger(handler.result) logger.log() 画面表示についての説明は割愛します

Slide 26

Slide 26 text

最終的なmain.kt fun main(args: Array) { val homeDir = System.getProperty("user.home") ?: "" val cd = File(".").absoluteFile.parent val javaList = SourceList(Language("Java", "java"), false) val kotlinList = SourceList(Language("Kotlin", "kt"), true) val ignoreFileReader = IgnoreFileReader("$homeDir/.krsignore", cd) ignoreFileReader.read() val handler = SearchHandler(cd, arrayOf(javaList, kotlinList), ignoreFileReader.files) handler.execute() val logger = ResultLogger(handler.result) logger.log() }

Slide 27

Slide 27 text

jarをビルド task packJar(type: Jar) { manifest { attributes 'Main-Class': 'jp.co.youmeee.app.MainKt' } from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } with jar } $ ./gradlew packJar 1. jarファイル生成のtaskを書いてあげて、 2. gradlew wrapperでタスク実行 3. build/libs配下にjarができる

Slide 28

Slide 28 text

jarを叩きやすくする ・生成されたjarを実行するには`java -jar krs.jar`みたいな感じで打つのは面 倒。。 ・今回は、homebrewで配布する方法で簡単に叩けるようにしました。(方法は 割愛します) $ ./AndroidStudio/HogeApp> krs

Slide 29

Slide 29 text

完成! 計算対象となるファイ ル一覧を出力 Projectルートで、 コマンド叩く 移行率の表示

Slide 30

Slide 30 text

まだKotlinにできるものがありますね、、? せっかくなので、、 build.gradle→build.gradle.kts にしてみました

Slide 31

Slide 31 text

build.gradle.ktsへの移行 ・GradleのビルドスクリプトをKotlinのDSLで書ける! ・既存のbuild.gradleファイルを消して、新たにbuild.gradle.ktsファイルを作成 ・こちらのマイグレーションガイドが参考になりました。 https://guides.gradle.org/migrating-build-logic-from-groovy-to-kotlin/

Slide 32

Slide 32 text

build.gradle buildscript { ext.kotlin_version = '1.2.10' repositories { mavenCentral() } dependencies { classpath "org.jetbrains.kotlin: kotlin-gradle-plugin:$kotlin_version" } } plugins { id 'org.jetbrains.kotlin.jvm' version '1.1.51' id 'com.github.johnrengelman.shadow' version '2.0.1' } apply plugin: 'application' apply plugin: 'java' group 'jp.co.youmeee' version '1.2' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testCompile group: 'junit', name: 'junit', version: '4.12' compile 'org.slf4j:slf4j-api:1.7.25' compile 'org.slf4j:jcl-over-slf4j:1.7.25' compile 'org.slf4j:jul-to-slf4j:1.7.25' compile 'ch.qos.logback:logback-classic:1.2.3' } task packJar(type: Jar) { manifest { attributes 'Main-Class': 'jp.co.youmeee.app.MainKt' } from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } with jar }

Slide 33

Slide 33 text

build.gradle.kts buildscript { val kotlinVersion = "1.2.10" repositories { mavenCentral() } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin: $kotlinVersion") } } group = "jp.co.youmeee" version = "1.2" plugins { java application `kotlin-dsl` id("org.jetbrains.kotlin.jvm") version "1.1.51" id("com.github.johnrengelman.shadow") version "2.0.1" } java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } repositories { mavenCentral() } dependencies { val kotlinVersion = "1.2.10" compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") testCompile(group = "junit", name = "junit", version = "4.12") compile ("org.slf4j:slf4j-api:1.7.25") compile("org.slf4j:jcl-over-slf4j:1.7.25") compile("org.slf4j:jul-to-slf4j:1.7.25") compile("ch.qos.logback:logback-classic:1.2.3") } task("packJar", type = Jar::class) { archiveName = "krs.jar" manifest { attributes["Main-Class"] = "jp.co.youmeee.app.MainKt" } from(configurations.runtime.map({ if (it.isDirectory) it else zipTree(it) })) with(tasks["jar"] as CopySpec) }

Slide 34

Slide 34 text

全てKotlinで書くことができました!

Slide 35

Slide 35 text

まとめ、感想 ・Kotlinでもコマンドラインツールは作れる!! ・File系のクラスはAndroid書いてるとあまり触らないし、便利な拡張関数もあって、 勉強になりました。 ・エラーハンドリングとか甘い部分あるので、その辺りfixしていきたい。 ・もう少し汎用的にして、外からオプションで、言語の識別子を与えるようにすれば、 Java→Kotlinのようなこともできる(需要はない) https://github.com/youmitsu/KotlinReplaceSupport er

Slide 36

Slide 36 text

参考資料 ・Migrating build logic from Groovy to Kotlin https://guides.gradle.org/migrating-build-logic-from-groovy-to-kotlin/ ・JVMベースのコマンドラインツールをHomebrewで配布する https://int128.hatenablog.com/entry/2015/02/27/220907