TypeScript環境でnycとtapeを使って、カバレッジを取るテスト環境を整える。

初めはavaを使おうと思ってたんです。

下記の説明を見たところコンパイルしてからやってね、みたいな形になっています。 github.com

github.com

avaはmagic-assertを使っており、jsに対してassertの表示を見やすくするための処理が入ってるはずです。 そのため、babel等を使う前提で作られています。そのため、TypeScriptへの対応はまだされていない模様です。

というわけで諦めて、tapeとts-nodeとnycでカバレッジを取ってみます。

yarn add tape ts-node nyc typescript @types/tape -D

package.jsonにnpm scriptsを記述します。 今回はコンソールに出力したいのとhtmlでも出力してみたいので reporterを2つ指定してあります。

{
  ....
  "scripts":{
     "test": "nyc --reporter=html --reporter=text tape src/test/front/**.ts"
  }
}

同じく、package.jsonにnycの設定を追加します。

{
  ....
  "nyc":{
    "include": [
      "src/main/front/**/*.ts"
    ],
    "extension": [
      ".ts"
    ],
    "require": [
      "ts-node/register"
    ],
    "all": true
  }
}

サンプルコードとテストコードです。 tapeのendメソッドを呼ばなくても動きそうだと思ったのですがうまくいかず・・・

// module.ts
export default function(){
  console.log("test")
  if(typeof window=="object"){
    console.log("not reached")
  }
}

// module.spec.ts
import * as test from "tape"
import module from "../../main/front/module" 
test('xxx', (t)=>{
  module()
  t.isEqual("a","b")
  t.end()
})
npm test

こんなログが出ます。

PS D:\workspace\spring-sandbox\oauth> npm test

> spring-sandbox@1.0.0 test D:\workspace\spring-sandbox\oauth
> nyc --reporter=html --reporter=text tape src/test/front/**.ts

TAP version 13
# xxx
test
not ok 1 should be equal
  ---
    operator: equal
    expected: 'b'
    actual:   'a'
    at: Test.test (D:\workspace\spring-sandbox\oauth\src\test\front\test.ts:5:5)
  ...

1..1
# tests 1
# pass  0
# fail  1

----------|----------|----------|----------|----------|----------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
----------|----------|----------|----------|----------|----------------|
All files |       75 |       50 |      100 |       75 |                |
 test.ts  |       75 |       50 |      100 |       75 |              5 |
----------|----------|----------|----------|----------|----------------|
npm ERR! Test failed.  See above for more details.

htmlのカバレッジレポートもこんな感じで出力されました。 f:id:reteria:20170302041658p:plain

まとめ

avaが使いたかったところから迷走した感があります。

よくよく考えたらTypeScript使うので jasmineでいい気がした。(apiがわからない問題は解決するはず) つまり、カバレッジも取れそうだし jasmineベースのjestでいい気がしました。

また、環境構築し直しです。

アノテーションを体が欲している:NotNull:NotNull:NotNull

みんなだいすきアノテーション みんな大嫌いアノテーション あの手この手でアノテーション

というわけでNotNullアノテーションが付けられたメソッドを バイトコード操作する話です。

NotNull制約をアノテーションだけで実現します。

似たような仕組みはChecker Frameworkだったり Bean Validationだったり、Apache PolygeneのConstraintだったり あるわけでして。

まぁそんな仕組みを書いてみたかった、という話になります。

今回の話は紆余曲折します。

アノテーションの書ける場所の話

現在、Java8では色んな箇所にAnnotationを記述することができます。

JVM Specificationには以下のサンプルが途中都合上出てきます。 「Outer.Middle.@Foo Inner」なんて、普段使わない気もするのですが 面白いですね。

一番上のStringの二次元の配列の例はどこにAnnotation書いてるのか分かりにくい・・・

@Foo String[][]
String @Foo [][]
String[] @Foo []

@Foo Outer.Middle.Inner
Outer.@Foo Middle.Inner
Outer.Middle.@Foo Inner

@Foo Map<String,Object>
Map<@Foo String,Object>
Map<String,@Foo Object>

List<@Foo ? extends String>
List<? extends @Foo String>

さて、そんなこんなでJava8ではメソッド(コンストラクタ)の引数に自分の型を書くことができます。 レシーバパラメータと呼ばれる機能のようです。 JavaSE8リリース記念!マイナーな言語仕様を紹介してみる(交差型キャスト,レシーバパラメータ(仮引数にthis)) - きつねとJava!

使い方については、以下のようにアノテーションをつけることを想定されたような機能です。 Checker Frameworkのようにアノテーションで型検査をするためにつけるとかでしょうね。

class Test{
  void m(Testt his, int n1, int n2){}
}
class Test{
  void m(@Sample Test this, int n1, int n2){}
}

TypeScriptも似たような機能があって、 こちらはJavaScriptらしさがあります。 Playground · TypeScript

ECMAScriptのほうにも似たような話が上がってるみたいです。(このリポジトリbind-operatorなんですけどね) Explicit naming of `this` · Issue #30 · tc39/proposal-bind-operator · GitHub

こっちはthisに別名をつけられるようにしません?みたいな話で JavaScriptではthisがコロコロ変わることがあるので、下のコードみたいに一時変数への代入をしたりします。

function join(){
   const array = this;
   return array.join(", ");
}

これに合わせてflow typeのtype annotationで型検査できるとか楽しそうですね。 (現状、似たような機能ってあるんですかね?なんかある気もするけどぱっと見、見つからなかった)

アノテーションにthisを使える、みたいなのはあるようですが。 Flow | Classes

NotNullアノテーションがあるメソッドに対してnullのチェックを追加する

これはお題目通りのコードです。

今回はバイトコード操作で書いてみます。 方法としてはメソッドのインターセプトとかでも書けそうですね。 InjectionPointからMethodが取れるのでそこからgetParameterAnnotations()アノテーションが取れるので・・・ ってそっちからの方が良かったのでは・・・

下のようなサンプルのコードを用意しました。

public class Example {
  public static void staticMethod(@NotNull String notNullString) {}

  public void method(@NotNull String notNullString) {}

  public void thisAnnotated(@NotNull Example this,@NotNull String notNullString) {}

}

では、これをこんな感じにしましょう。

public class Example {
  public static void staticMethod(@NotNull String notNullString) {
    Objects.requireNonNull(notNullString, "notNullString is required");
  }

  public void method(@NotNull String notNullString) {
    Objects.requireNonNull(notNullString, "notNullString is required");
  }

  public void thisAnnotated(@NotNull Example this,@NotNull String notNullString) {
    Objects.requireNonNull(this, "this is required");
    Objects.requireNonNull(notNullString, "notNullString is required");
  }

}

できませんでした。 こうなります。

public class Example {
  public static void staticMethod(@NotNull String notNullString) {
    Objects.requireNonNull(notNullString, "notNullString is required");
  }

  public void method(@NotNull String notNullString) {
    Objects.requireNonNull(notNullString, "notNullString is required");
  }

  public void thisAnnotated(@NotNull Example this,@NotNull String notNullString) {
    Objects.requireNonNull(notNullString, "notNullString is required");
  }

}

気を取り直して実際書いたコードを下に示します。 javaagentで利用する想定で書いたのでClassFileTransformerを実装しています。

バイトコードの操作にはjavassistを使いました。

public class NotNullInstrumentation implements ClassFileTransformer {
  public ClassPool pool;

  public NotNullInstrumentation(ClassPool pool) {
    this.pool = pool;
  }

  @Override
  public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
    byte[] classfileBuffer) throws IllegalClassFormatException {
    try (ByteArrayInputStream stream = new ByteArrayInputStream(classfileBuffer)) {
      CtClass clazz = pool.makeClass(stream);
      for (CtMethod method : clazz.getDeclaredMethods()) {
        MethodInfo info = method.getMethodInfo();
        CodeAttribute codeAttribute = info.getCodeAttribute();
        LocalVariableAttribute attribute = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
        Object[][] annotationArray = method.getParameterAnnotations();
        for (int i = 0; i < annotationArray.length; i++) {
          Object[] annotations = annotationArray[i];
          String name = attribute.variableName(i + (Modifier.isStatic(method.getModifiers()) ? 0 : 1));
          for (Object annotation : annotations) {
            if (annotation instanceof NotNull) {
              method.insertBefore("java.util.Objects.requireNonNull($" + (i + 1) + ",\"" + name + " is required\");");
            }
          }
        }
      }
      return clazz.toBytecode();
    } catch (IOException | ClassNotFoundException | CannotCompileException e) {
      throw new RuntimeException(e);
    }
  }
}

宣言されたメソッドとそのメソッドの引数のアノテーションを見て NotNullアノテーションを見てメソッドの先頭に処理を追加しています。

今回実装した、nullを検査するコードは実は不完全でもう少し注意深くコードを書かないといけません。 クラスパスにNotNullアノテーションが入っていないとエラーになってしまいます。 やるとしたら、完全修飾クラス名を取得して比較とかするんでしょうね。

横道:ローカル変数名の取得

LovalVariableAttributeはローカル変数テーブルを表しています。 なぜここからデータを取っているかというと Javacはオプションを渡さない限り、メソッドの引数名を残しません。 このクラスのgetNameでは、コンパイルオプションを渡してない限り、変数名は帰ってきません。(argNになります) Parameter (Java Platform SE 8 )

テストコード

このコードをテストします。といってもエラーがでないことを確認するだけです。 今回のお供はjMockitさんです。

  @Test
  public void test() throws Exception {
    new MockUp(Class.forName("javassist.CtClassType")) {
      @Mock
      public boolean isFrozen() {
        return false;
      }
    };
    NotNullInstrumentation inst = new NotNullInstrumentation(ClassPool.getDefault());
    inst.transform(getClass().getClassLoader(), forInstruments(Example.class), null, null, getByteCode(Example.class));
  }

なぜisFrozenにmockupをしているかというと 実行中にClassの書き換えを行うのでjavassistから例外がスローされるからです。

(そういえば、jacocoとjmockit入れた時に気づいたのですが、jmockitVM実行中にjavaagentを与えているようですね。何やらイケない香りがしますね。)

で、先ほどのコードでなぜthisのrequireNonNullが出力できないのかという話をします。 その前にjavapします。

Annotationさんや~どこぞ~

ちょっとテストのためにメソッドを追加した以下のクラスをjavapしてみます。

public class Example {
  public static void staticMethod(@NotNull String notNullString) {}

  public void method(@NotNull String notNullString) {}

  public void thisAnnotated(@NotNull Example this,@NotNull String notNullString) {}

  @NotNull
  public void methodAnnotated(@NotNull Example this,@NotNull String notNullString) {}

  public List<@NotNull String> typeAnnotated() {
    return null;
  }
}

下のような情報が出てきます。

~~~~
  public static void staticMethod(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    RuntimeVisibleParameterAnnotations:
      parameter 0:
        0: #17()
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0 notNullString   Ljava/lang/String;
    RuntimeVisibleTypeAnnotations:
      0: #17(): METHOD_FORMAL_PARAMETER, param_index=0

  public void method(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    RuntimeVisibleParameterAnnotations:
      parameter 0:
        0: #17()
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/github/wreulicke/bean/validation/Example;
            0       1     1 notNullString   Ljava/lang/String;
    RuntimeVisibleTypeAnnotations:
      0: #17(): METHOD_FORMAL_PARAMETER, param_index=0

  public void thisAnnotated(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    RuntimeVisibleParameterAnnotations:
      parameter 0:
        0: #17()
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 12: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/github/wreulicke/bean/validation/Example;
            0       1     1 notNullString   Ljava/lang/String;
    RuntimeVisibleTypeAnnotations:
      0: #17(): METHOD_FORMAL_PARAMETER, param_index=0
      1: #17(): METHOD_RECEIVER

  public void methodAnnotated(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    RuntimeVisibleAnnotations:
      0: #17()
    RuntimeVisibleParameterAnnotations:
      parameter 0:
        0: #17()
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 15: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/github/wreulicke/bean/validation/Example;
            0       1     1 notNullString   Ljava/lang/String;
    RuntimeVisibleTypeAnnotations:
      0: #17(): METHOD_FORMAL_PARAMETER, param_index=0
      1: #17(): METHOD_RECEIVER

  public java.util.List<java.lang.String> typeAnnotated();
    descriptor: ()Ljava/util/List;
    flags: ACC_PUBLIC
    Signature: #28                          // ()Ljava/util/List<Ljava/lang/String;>;
    Code:
      stack=1, locals=1, args_size=1
         0: aconst_null
         1: areturn
      LineNumberTable:
        line 18: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       2     0  this   Lcom/github/wreulicke/bean/validation/Example;
    RuntimeVisibleTypeAnnotations:
      0: #17(): METHOD_RETURN, location=[TYPE_ARGUMENT(0)]
}

おやおや色々情報が出てきました。

今回見たいのはレシーバパラメータのメソッドなので少し絞ってみます。

  public void thisAnnotated(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    RuntimeVisibleParameterAnnotations:
      parameter 0:
        0: #17()
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 12: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/github/wreulicke/bean/validation/Example;
            0       1     1 notNullString   Ljava/lang/String;
    RuntimeVisibleTypeAnnotations:
      0: #17(): METHOD_FORMAL_PARAMETER, param_index=0
      1: #17(): METHOD_RECEIVER

RuntimeVisibleParameterAnnotationsには情報が一つついていますね。(あれ?レシーバパラメータのあのテーションは?) また、先ほどお話したLocalVariableTableには変数名が残っています。 先ほどのコードではここからデータを取り出した形になっています。

17の情報はConstantPoolに保存されています。

おや、#17にはLjavax/validation/constraints/NotNull;ですね。 情報をこんな感じでテーブルにして保持しているのですねー。

Constant pool:
   #1 = Class              #2             // com/github/wreulicke/bean/validation/Example
   #2 = Utf8               com/github/wreulicke/bean/validation/Example
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          // "<init>":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/github/wreulicke/bean/validation/Example;
  #14 = Utf8               staticMethod
  #15 = Utf8               (Ljava/lang/String;)V
  #16 = Utf8               RuntimeVisibleParameterAnnotations
  #17 = Utf8               Ljavax/validation/constraints/NotNull;
  #18 = Utf8               notNullString
  #19 = Utf8               Ljava/lang/String;
  #20 = Utf8               RuntimeVisibleTypeAnnotations
  #21 = Utf8               method
  #22 = Utf8               thisAnnotated
  #23 = Utf8               methodAnnotated
  #24 = Utf8               RuntimeVisibleAnnotations
  #25 = Utf8               typeAnnotated
  #26 = Utf8               ()Ljava/util/List;
  #27 = Utf8               Signature
  #28 = Utf8               ()Ljava/util/List<Ljava/lang/String;>;
  #29 = Utf8               SourceFile
  #30 = Utf8               Example.java
{

で、ですね結局、レシーバパラメータの情報が引数のアノテーションと一緒に取れないのは 入ってるところが別だから、ですね。

引数のアノテーションはRuntime(In)VisibleParameterAnnotations アノテーション全般はRuntime(In)VisibleTypeAnnotationsに入っています。

引数のアノテーションはRuntime(In)VisibleParameterAnnotationsのアノテーションを取得しているからですね~ ローカル変数のアノテーションもこちらに入っているようです。

で、ですね。一応、JavassistにもRuntimeVisibleTypeAnnotationsに対応する TypeAnnotationsAttributeが入っています。

が、現状のJavassistAPIではアノテーションの数しか取れません。 また、アノテーションの配置場所とアノテーションが一緒に取れないといけません。 なかなか骨が折れそうです。 (ローカル変数や型パラメータに対するアノテーションの情報もここに入るので)

asmなら少し調べたところ、触れそうですね。 MethodVisitor (ASM 5.1 Documentation)

visitTypeAnnotationとvisitLocalVariableAnnotationというAPIが存在します。

まとめ

JavapしてJVM Specificationとにらめっこした話、でした。

ASM触ればいい気がしてきた。

みんなもっとアノテーション塗れになろうや!!!!!

備忘録としてここに書いておきます。

JavaFX入門した

やる気ない感じでJavaFXを軽く触ることにした。

f:id:reteria:20170216011414p:plain

ごらんの有様だよ!!

ドラッグ・アンド・ドロップしたファイル(.jar or .class)をデコンパイルします。
ドラッグ・アンド・ドロップしたファイルと同じディレクトリにdecompiledというフォルダを作成し
そこにデコンパイルされたソースを展開します。

一部デコンパイルできないケースがあると思いますが
ライブラリが対応してないので許してください。。。

というわけでJavaFXデコンパイルするだけのアプリケーションを作ったよ、という話でした。

リポジトリはこちらになります。
GitHub - Wreulicke/decompiler-javafx-application: JavaFX Application for decompile tool using windup-procyon

余談

本番環境にしかソースがなくて困る!!デコンパイルしたい!!
コード書くのだるい!!JavaFXで適当にpackageして放り投げたい!!
そんな熱い気持ちで書きました。