Selenium + Internet Explorer 11 でファイルダウンロード

Selenium WebDriver で Internet Explorer 11 を操作し、ファイルダウンロードをしようとした時にハマったのでメモ。

問題

IE 11 でファイルをダウンロードした場合、状況によって 3 種類の画面が表示されます。

  1. ダウンロードダイアログ
  2. 通知バー
  3. ダウンロードの表示 ダイアログ

これらの画面がどういうもので、どういう状況で表示されるかについては、IE サポートチームのブログ記事をご覧ください。

ファイル ダウンロード時の通知バー、ダイアログ、ダウンロードの表示と追跡について – Japan IE Support Team Blog

Selenium + IEDriver でファイルをダウンロードした場合、上記画面が表示されると WebDriver を動かしているスレッドが停止してしまいます。

StackOverflow で調べると、その画面を操作するには java.awt.Robot を使いボタン操作で対応するという回答がありますが、そもそも処理が停止されているので Robot の処理が動きません。

対応

3 種類の画面のうち、いくつか試したところほぼ 1 と 2 が表示されたため、それらに対する対応方法です。

Robot の操作を別スレッドで処理し、ダウンロード画面を操作してメインスレッドを復帰させます。

以下、Kotlin でのサンプルコードです。

import org.openqa.selenium.ie.InternetExplorerDriver
import java.awt.Robot
import java.awt.event.KeyEvent
import java.nio.file.Files
import java.nio.file.Paths
import java.time.LocalDateTime
import kotlin.concurrent.thread

fun main(args: Array<String>) {

    // IEドライバー設定
    System.setProperty("webdriver.ie.driver", "C:\\Selenium\\InternetExplorer\\IEDriverServer.exe")

    val driver = InternetExplorerDriver()

    driver.get("http://example.com/files")
    driver.get("http://example.com/files/bigdata.csv")

    // 本ブログ記事のポイント
    // 別スレッドで、Alt + S ボタンを送信し、ファイルダウンロードする。
    thread {
        Thread.sleep(2000) // ダウンロード画面が表示されるのを待つ。
        val robot = Robot()
        robot.autoDelay = 250
        robot.keyPress(KeyEvent.VK_ALT)

        Thread.sleep(1000)
        robot.keyPress(KeyEvent.VK_S)
        robot.keyRelease(KeyEvent.VK_ALT)
        robot.keyRelease(KeyEvent.VK_S)
    }

    // driver#close() を呼び出すとブラウザが終了しファイルダウンロードもされないので、ファイルがダウンロードされるまで待機。
    // これを WebDriver 側で検知する方法を見つけられておらず、ファイル名で確認するしかなさそう。
    val downloadedFilePath = "C:\\Users\\rabitarochan\\Downloads\\bigdata.csv"
    val path = Paths.get(downloadedFilePath)
    while (!Files.exists(path)) {
        println("${LocalDateTime.now()} - ファイルがありません。")
        Thread.sleep(1000)
    }

    driver.close()

上記コードでは、ファイルダウンロード中にブラウザーが終了しないよう、ダウンロードファイルの作成をチェックしています。 IEDriver では、おそらくファイルダウンロード先を実行時に制御することはできないため、ユーザープロファイル以下の Downloads フォルダーにファイルが作成されるかどうかチェックしています。

まとめ

IE 縛りの Web ページ辛い。

参考サイト

Array を指定した個数で分割する ChunkPipe を作った

作ったってほどの話ではないです。

Bootstrap を使っていると、Array に入っている要素からグリッドを作成していことがあります。あるんです。

<!-- こんなデータがあったとして

let items = [
  { name: "はてなブログ", url: "http://hatenablog.com/" },
  { name: "はてなブックマーク", url: "http://b.hatena.ne.jp/" },
  { name: "人力検索はてな", url: "http://q.hatena.ne.jp/" },
  { name: "はてなアンテナ", url: "http://a.hatena.ne.jp/" },
  { name: "はてなキーワード", url: "http://k.hatena.ne.jp/" }
];

-->
<div class="container">
  <div class="row">
    <!-- container > row の中に col-md-4 のグリッドが 5 つ入ってしまう -->
    <div class="col-md-4" *ngFor="let item of items"> 
      <p><a [href]="item.url">{{ item.name }}</a></p>
    </div>
  </div>
</div>

Bootstrap 的には以下のように定義するべきだと思います。 (テンプレート展開後)

<div class="container">
  <div class="row">
    <div class="col-md-4"> 
      <p><a href="http://hatenablog.com/">はてなブログ</a></p>
    </div>
    <div class="col-md-4"> 
      <p><a href="http://b.hatena.ne.jp/">はてなブックマーク</a></p>
    </div>
    <div class="col-md-4"> 
      <p><a href="http://q.hatena.ne.jp/">人力検索はてな</a></p>
    </div>
  </div>
  <div class="row">
    <div class="col-md-4"> 
      <p><a href="http://a.hatena.ne.jp/">はてなアンテナ</a></p>
    </div>
    <div class="col-md-4"> 
      <p><a href="http://k.hatena.ne.jp/">はてなキーワード</a></p>
    </div>
  </div>
</div>

div.rowdiv.col-md-4 の 2 つのループが登場することになります。 まず元の Array を 3 個に分割して、その要素をループして・・・となりますが、元の Array を分割する処理をどこに書くか ? という問題が起きました。

今回は、Array を指定した個数で分割する ChunkPipe を定義して使ってみようと思います。

定義

ChunkPipe の定義は以下のとおりです。

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'chunk'
})
export class ChunkPipe implements PipeTransform {

  transform(value: any, args?: any): any {
    let array = value;
    let count = args;

    let len = Math.round(array.length / count);
    let chunked = [];
    for (let i = 0; i < len; i++) {
      chunked.push(array.slice(i * count, i * count + count))
    }
    return chunked;
  }

}

PipeTransform を実装した ChunkPipe クラスを定義します。 Pipe の定義方法は以下のドキュメントを参照してください。

Pipes - ts - GUIDE

このような処理は、PHP には array_chunk 関数で用意されています。 もちろん、NPM にもあります。

www.npmjs.com

使い方

row のループをする際に、ChunkPipe でまず Array を分割して、その後分割した Array で div.col-md-4 をループします。

<div class="container">
  <!-- row を ngFor でループする際に、chunk:3 を指定し、要素数が 3 個の Array を生成する。 -->
  <div class="row" *ngFor="let chunked of items | chunk:3">
    <!-- col-md-4 をループする -->
    <div class="col-md-4" *ngFor="let item of chunked"> 
      <p><a [href]="item.url">{{ item.name }}</a></p>
    </div>
  </div>
</div>

すると、あるべき姿の HTML を生成することができます。

注意点

今回定義した Pipe は、pure な Pipe を定義しています。 Array 自体や Array の中身を変更しても画面に表示されない場合、impure なPipe に変更すると上手くいくかもしれません。

pureimpure については、@mitsuruog さんのブログ記事で解説されています。

blog.mitsuruog.info

ただし、性能はちょっと悪くなる可能性もあります。

まとめ

Angular で Array を表示する際のちょっとした小ネタでした。 Component 側で分割した Array を作っている方などはぜひ使ってみてください。

DatePipe を IE11 等で動かすと変な表示になる件に対応する

Angular で日付をフォーマット表示する場合、DatePipe を利用します。 Pipe とは、AngularJS で言う Filter で、構文も変わっていません。

<p>Date: {{ now | date:'yyyy-MM-dd HH:mm:ss' }}</p>

さて、このシンプルなフィルターですが、ブラウザごとに異なる表示になってしまいます。 以下の URL を開いてみてください。

https://plnkr.co/edit/lVCkJcIopxTOk5L9iyCm?p=preview

どうでしたか ? IE11, Edge の方は上手く表示されていないのではないかと思います。

Expected: 2017-04-01 12:34:56

Actual: 2017-04-01 12:00:4/1/2017 12:34:56 PM:4/1/2017 12:34:56 PM

どーやったらこんな表示になるんだ・・・。

どうやら不具合っぽい

もちろん、Issue は立っています。

date pipe formatter doesn't format minutes and seconds properly in IE 11.0.9600.18350 and Edge · Issue #9524 · angular/angular · GitHub

また、ドキュメントには以下の通り書いてあります。

DatePipe - ts - API

WARNINGS:

  • this pipe is marked as pure hence it will not be re-evaluated when the input is mutated. Instead users should treat the date as an immutable object and change the reference when the pipe needs to re-run (this is to avoid reformatting the date on every change detection run which would be an expensive operation).
  • this pipe uses the Internationalization API. Therefore it is only reliable in Chrome and Opera browsers.

重要なのは最後の 1 文。 DatePipe は I18n API を利用しているので、ChromeOpera でのみ信頼性があります。

Workaround: moment を利用する

Issue にもコメントがありますが、Workaround として以下 2 つが提示されています。

  • date:'short'date:'shortDate' はちゃんと動くよ !
  • moment を使った Pipe を定義しよう !

任意のフォーマットで表示したいのに date:'short' は動くと言われても困ります。

ということで、 moment を使った Pipe を定義しましょう。

1. npm install

はじめに、moment への依存設定を追加します。

npm install moment --save

なお、TypeScript の型定義は moment に含まれているため、別途 @types/moment を導入する必要はありません。(導入すると、追加しなくていいよと警告が表示されます。)

2. Pipe 実装

以下のように Pipe を実装します。

angular-cli をご利用の方は ng g pipe moment.pipe で雛形を生成できます。

import { Pipe, PipeTransform } from '@angular/core';
import * as moment from 'moment';  // (1)

@Pipe({
  name: 'moment' // (2)
})
export class MomentPipe implements PipeTransform {

  transform(value: any, args?: any): any {
    if (value === undefined || value === null) return '# value is undefined or null #';  // (3)
    if (args === undefined || args === null) return '# args is undefined or null #'  // (4)
    let s = moment(value).format(args);
    return s;
  }

}
No 解説
(1) moment を参照します
(2) name に指定した名前で Pipe を利用できます
(3) 日付 or 日付形式の文字列が定義されているかのチェックです
(4) 引数 (フォーマット文字列) が指定されているかのチェックです

3. Module 登録

Pipe を実装したら、忘れずに Module に登録します。

angular-cli を使って生成した場合は追加されているかも。

以下は、Pipe をまとめた PipesModule を定義した場合の例です。

// import 省略

@NgModule({
  imports: [
    CommonModule
  ],
  exports: [
    MomentPipe  // (1)
  ],
  declarations: [
    MomentPipe  // (2)
  ]
})
export class PipeModule { }
No 解説
(1) PipeModule を import したモジュールで MomentPipe を利用したい場合、exports に追加する必要があります。
(2) declarations に MomemtPipe を追加し、モジュール内で利用できるようにします。

まとめ

IE11 等で DatePipe でフォーマット指定がうまくいかないため、代替案として moment を利用した Pipe を作成しました。

DatePipe で困っている方は参考にしてみてください。

Angular で複数回発生するイベントを抑制する (debounce)

最近は Angular と戯れています。

Qiita の自動保存のように、入力イベントを検知し、最後の入力から指定時間経過した後に何らかの処理を実行する方法についてメモ。

Component クラス

Component クラスにて、入力イベントを扱う Subject を定義します。

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';

@Component(/* 省略 */)
export class TestComponent implements OnInit, OnDestroy {

  // イベントをまとめる Subject
  autoSaveEvent: Subject<void> = new Subject<void>();

  // Subscribe 情報
  autoSaveSubscription: Subscription = null;

  ngOnInit() {
    // "debounceTime()" をコールし、イベントを 5000 ミリ秒抑制してから処理する。
    this.autoSaveSubscription =
      this.autoSaveEvent.debounceTime(5000).subscribe(
        () => { /* 何らかの処理 */ }
      );
  }

  ngOnDestroy() {
    // unsubscribe しないと、別の Component に遷移してもイベントが発行してしまうので注意。
    if (this.autoSaveSubscription != null) {
      this.autoSaveSubscription.unsubscribe();
    }
  }

}

Component クラスでのポイントは以下 2 点です。

  1. Subject<T>#debounceTime(milliSeconds: number) を呼び出し、イベントを指定時間抑制する。
  2. 不要になったら (上記例では、Component のインスタンスが廃棄されたタイミング) unsubscribe する。

特に 2 が重要です。Component が廃棄されても発火したイベントや Subscribe は自動で解除されません。

Template

Template では、Input 要素のイベント発生時に Subject<T>#next() をコールし、イベントを発生させます。

<input type="text" name="input1" [(ngModel)]="input1" (keyup)="autoSaveEvent.next()">
<textarea name="input2" [(ngModel)]="input2" (keyup)="autoSaveEvent.next()">

ここでのポイントは 2 点です。

  1. (keyup) で、keyup イベントにバインドする
  2. Component クラスに Subject を用意したため、複数のイベントを 1 つのイベントソースのようにみなして処理できる。

Kotlin Compiler Plugin (kotlin-allopen) を追いかける

これは Kotlin AdventCalendar 2016 の 24 日目の記事です。メリークリスマスイブ!時間的にギリギリアウトでした・・・。

さて、少し前に Kotlin 1.0.6 RC が出ましたが、そのタイミングで kotlin-allopenkotlin-noarg という 2 つのコンパイラプラグインが発表されました。

discuss.kotlinlang.org

それぞれ、以下の働きをするコンパイラプラグインとなっています。

  • kotlin-allopen
    任意のアノテーション (ユーザーにて指定できます) が付いているクラスのメンバーに、暗黙的に open 修飾子を付与します。 Spring 等のフレームワークや Mockit などのライブラリに有益な物となっています。普段 Web アプリ開発に Kotlin + Spring Framework を使っているのでとても助かりそうです!

  • kotlin-noarg
    任意のアノテーション (ユーザーにて指定できます) が付いているクラスに、暗黙的に引数ゼロのコンストラクターを生成します。 主に JPA 対応として作成されたようです。

コンパイラプラグインの使い方は上のリンクを参照していただくとして、今回は kotlin-allopen プラグインを題材に、コードにコメントを追加する形でコンパイラプラグインの作りについて追いかけてみたいと思います。リンクは、タグ v1.1-M04 です。 (v1.0.6 RC はタグが切られてないようなので、2016/12/24 時点で一番新しいタグをベースにしました。)

全体の仕組み

kotlin-allopen プラグインに関するコードは以下のディレクトリにあります。kotlin-allopen の本体は共通化されており、Gradle と Maven 向けのプラグイン実装が用意されています。

今回はプラグイン本体のみ追いかけます。

プラグイン本体

ここから、プラグイン本体のコードを読んでいきます。コードは 2 ファイルのみで、大きくないためすぐ読めます。

AllOpenPlugin.kt

AllOpenPlugin.kt には、コンパイラプラグインに必要な 2 つのインターフェースを実装したクラスが定義されています。 コンパイラプラグインに必要なインターフェースは以下の 2 つです。

/*
 * Copyright 2010-2016 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jetbrains.kotlin.allopen

import com.intellij.mock.MockProject
import org.jetbrains.kotlin.compiler.plugin.CliOption
import org.jetbrains.kotlin.compiler.plugin.CliOptionProcessingException
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.CompilerConfigurationKey
import org.jetbrains.kotlin.extensions.DeclarationAttributeAltererExtension

/**
 * コンパイラープラグインで扱う設定のキーを定義するオブジェクトです。
 */
object AllOpenConfigurationKeys {

    /**
     * kotlin-allopen の対象とするアノテーションのリストを設定するキーです。
     */
    val ANNOTATION: CompilerConfigurationKey<List<String>> =
            CompilerConfigurationKey.create("annotation qualified name")

}

/**
 * kotlin-allopen プラグインに指定されたオプション引数を処理するクラスです。
 */
class AllOpenCommandLineProcessor : CommandLineProcessor {

    /**
     * AllOpenCommandLineProcessor のコンパニオンオブジェクトです。
     */
    companion object {

        /**
         * オプション引数 annotation の定義です。
         *
         *   - オプション引数名は annotation です。
         *   - オプション引数には、プラグイン処理の対象とするアノテーションの完全修飾名を指定します。
         *   - オプション引数は複数指定可能です。
         *
         * コマンドラインコンパイラーを使用する場合、以下の引数を指定してコンパイラープラグインを有効にします。
         *
         *   `-P <jar ファイルのパス>:<プラグイン ID>:<オプション引数名>=<アノテーションの完全修飾名>[, 複数指定する場合はカンマで区切る]`
         */
        val ANNOTATION_OPTION = CliOption("annotation", "<fqname>", "Annotation qualified names",
                                          required = false, allowMultipleOccurrences = true)

        /**
         * コマンドラインコンパイラーで指定するプラグイン ID を定義します。
         */
        val PLUGIN_ID = "org.jetbrains.kotlin.allopen"
    }

    /**
     * CommandLineProcessor インターフェースで定義されている変数です。
     * コンパニオンオブジェクトで定義したプラグイン ID を返します。
     */
    override val pluginId = PLUGIN_ID

    /**
     * CommandLineProcessor インターフェースで定義されている変数です。
     * オプション引数のリストを返します。
     */
    override val pluginOptions = listOf(ANNOTATION_OPTION)

    /**
     * CommandLineProcessor インターフェースで定義されているメソッドです。
     * 指定されたオプション引数を処理します。
     *
     * オプション引数に指定した数だけ繰り返し実行されます。
     *
     * @param option オプション引数を判別するための CliOption です。
     * @param value オプション引数に指定された値です。
     * @param configuration コンパイラーの設定等を保持するオブジェクトです。
     */
    override fun processOption(option: CliOption, value: String, configuration: CompilerConfiguration) = when (option) {
        /* オプション引数 annotation が指定された場合の処理です。 */
        ANNOTATION_OPTION -> {
            /* これまでに設定されたオプション引数 annotation のリストを、CompilerConfiguration から取得します。
             * 設定を取得するキーは、AllOpenConfigurationKeys オブジェクトで定義しています。 */
            val paths = configuration.getList(AllOpenConfigurationKeys.ANNOTATION).toMutableList()
            /* 今回の値をリストに追加します。 */
            paths.add(value)
            /* CompilerConfiguration の値を上書きします。 */
            configuration.put(AllOpenConfigurationKeys.ANNOTATION, paths)
        }
        /* kotlin-allopen プラグインが想定していないオプションが指定された場合は例外をスローします。 */
        else -> throw CliOptionProcessingException("Unknown option: ${option.name}")
    }
}

/**
 * kotlin-allopen プラグインの処理を登録します。
 */
class AllOpenComponentRegistrar : ComponentRegistrar {

    /**
     * ComponentRegistrar インターフェースで定義されているメソッドです。
     * コンパイラープラグインの処理を登録します。
     *
     * @param project Intellij IDEA で用意しているオブジェクトのようですが、あまり情報がないため不明です・・・。
     * @param configuration コンパイラーの設定等を保持するオブジェクトです。
     *                      AllOpenCommandLineProcessor で処理済みのものです。
     */
    override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) {
        /* CompilerConfiguration から、kotlin-allopen 処理の対象とするアノテーションのリストを取得します。
         * 取得できない場合はプラグイン処理を登録しません。 */
        val annotations = configuration.get(AllOpenConfigurationKeys.ANNOTATION) ?: return
        if (annotations.isEmpty()) return

        /* kotlin-allopen 処理を登録します。 */
        DeclarationAttributeAltererExtension.registerExtension(project, CliAllOpenDeclarationAttributeAltererExtension(annotations))
    }
}

AllOpenDeclarationAttributeAltererExtension.kt

AllOpenDeclarationAttributeAltererExtension.kt では、kotlin-allopen の処理をおこなうクラスを定義しています。

コンパイラーの拡張ポイント (という名前でいいのか?) は DeclarationAttributeAltererExtension というインターフェースです。

コンパイラーのフロントエンドの org.jetbrains.kotlin.extensions パッケージに定義されているインターフェース / クラスは数少ないのですが、まだ十分に用意されていないのかそれともこれで十分なのか、理解が足りず分かりません・・・。

/*
 * Copyright 2010-2016 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jetbrains.kotlin.allopen

import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.extensions.AnnotationBasedExtension
import org.jetbrains.kotlin.extensions.DeclarationAttributeAltererExtension
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtModifierListOwner
import org.jetbrains.kotlin.resolve.BindingContext

/**
 * コンストラクターで指定されたアノテーションに対してプラグイン処理をおこなうクラスです。
 *
 * @property allOpenAnnotationFqNames プラグイン処理をおこなうアノテーションのリストです。
 */
class CliAllOpenDeclarationAttributeAltererExtension(
        private val allOpenAnnotationFqNames: List<String>
) : AbstractAllOpenDeclarationAttributeAltererExtension() {

    /**
     * AnnotationBasedExtension インターフェースで定義されているメソッドです。
     * プラグイン処理をするアノテーションのリストを返します。
     */
    override fun getAnnotationFqNames(modifierListOwner: KtModifierListOwner?) = allOpenAnnotationFqNames

}

/**
 * プラグイン処理をおこなうクラスです。
 *
 * 2 つのインターフェースを実装しています。
 *
 *   - DeclarationAttributeAltererExtension
 *   - AnnotationBasedExtension
 */
abstract class AbstractAllOpenDeclarationAttributeAltererExtension : DeclarationAttributeAltererExtension, AnnotationBasedExtension {

    /**
     * AbstractAllOpenDeclarationAttributeAltererExtension のコンパニオンオブジェクトです。
     */
    companion object {

        /**
         * テスト用のアノテーションリストを定義します。
         */
        val ANNOTATIONS_FOR_TESTS = listOf("AllOpen", "AllOpen2", "test.AllOpen")

    }

    /**
     * DeclarationAttributeAltererExtension インターフェースで定義されているメソッドです。
     * 
     * Modality を変更するプラグイン処理を実装します。(たぶん・・・)
     *
     * @property modifierListOwner
     * @property declaration
     * @property containingDeclaration
     * @property currentModality
     * @property bindingContext
     */
    override fun refineDeclarationModality(
            modifierListOwner: KtModifierListOwner,
            declaration: DeclarationDescriptor?,
            containingDeclaration: DeclarationDescriptor?,
            currentModality: Modality,
            bindingContext: BindingContext
    ): Modality? {
        /* 現在の Modality が FINAI 以外の場合はプラグインの処理をしません。
         * Modality は以下のものがあります。
         *   Modality.FINAL
         *   Modality.SEALED
         *   Modality.OPEN
         *   Modality.ABSTRACT */
        if (currentModality != Modality.FINAL) {
            return null
        }

        /* ここは理解していません。コンパイラーを追いかけないと分からなそうです・・・。 */
        val descriptor = declaration as? ClassDescriptor ?: containingDeclaration ?: return null

        /* コンストラクターで指定されたアノテーションが指定されている場合 */
        if (descriptor.hasSpecialAnnotation(modifierListOwner)) {
            /* final 修飾子が付いている場合は final のまま、それ以外の場合は open に変更します。 */
            return if (modifierListOwner.hasModifier(KtTokens.FINAL_KEYWORD))
                Modality.FINAL // Explicit final
            else
                Modality.OPEN
        }

        /* null を返すと、変更なし。 */
        return null
    }
}

まとめ

ソースにコメントを付けるという簡易的な形で、コンパイラプラグインの処理について追いかけてみました。

このプラグインの他にも、plugins ディレクトリにいくつか実装があります。またプラグインの他にも、Intellij IDEA 用のプラグインのようなコードもあるので、コンパイラプラグインに興味のある方は追いかけてみてください。

Kotlin 向けのテストライブラリ kotassert を作りました

何番煎じか分かりませんが、Kotlin 向けのテストライブラリを作りました。

さて、Kotlin で JUnit + hamcrest を使ってテストを書くと、予約後である is に悩まされます。

assertThat(actual, `is`(expected))

これを解決するために kotlintestknit というライブラリが出ています。

knit はご存知 @ngsw_taro さんのライブラリです。

taro.hatenablog.jp

// kotlintest
actual shouldBe expected

// knit
actual.should be expected

上記で予約後問題は解決されるわけですが、エイゴチットモデキナイ日本人的にはぱっと見て分かるメソッド名にしたいわけですよ。

しばらく悩んで・・・

予約語がだめなら、大文字にすればいいじゃない。」

ということで、メソッド名を大文字にした assertion を呼び出せるテストライブラリ kotassert を作りました。

github.com

使い方はとても簡単ですし、日本人でも読みやすいと思います。

ちなみに API は、普段 C# で開発するときに使っている Chaining Assertionパクって インスパイアしています。いつもありがとうございます。

// assertThat(actual, `is`(expected)) と同じ
actual.Is(expected)

// assertThat(actual, `is`(not(expected))) と同じ
actual.IsNot(expected)

// assertThat(actual, `is`(true)) と同じ
actual.IsTrue()

// assertThat(actual, `is`(notNullValue())) と同じ
actual.IsNotNull()

「メソッド名の先頭が大文字なんて邪道だ!!」という声もあると思います。私も重々承知しています。けどテストコードということでそこは大目に見てもらって・・・。

ある程度、私が必要だと思った hamcrest の Matcher は用意しています。どういう API が用意されているかは READMEテスト をご覧ください。

実装自体も、ただ JUnit + hamcrest を呼び出しているだけなので、すぐに読めると思います。拡張メソッド万歳。

良かったらお使いください。気に入ったらスターくれると喜びます。

Kotlin プロパティの Getter / Setter

Kotlin で、プロパティの Getter / Setter は以下の通りに定義できます。

var name: String
  get() { return ... }
  set(value) { ... }

定義する上でインデントは関係ないので、こんな感じでも書けちゃいます。

var name: String
get() { return ... }
set(value) { ... }

で?

せめてこうだったら読みやすかったのに、という感想です・・・。それだけです。

var name: String {
  get() { return ... }
  set(value) { ... }
}