雑賀 力王オフィシャルサイト

Abby CTO 雑賀 力王のオフィシャルサイトです

Semicolonless Java を実現する話

Semicolonless Java を実現する話

デンジャラス!ゾンビ!!!

こんにちは!ゲームマスターこと 檀 黎斗 です!

2000 年問題でバグスターウィルス見つけたの僕ですから!!

からのー

ジュリアナー!!トーキョー!

こんにちわ、ジョン・ロビンソンこと半ズボンの宇宙人です。

どうです?なんていうかアメブロっぽい感じっていうの?ムカつくでしょ??

そうでしょう!そうでしょう!

ところでみなさん、Java 書いてますか?Generics 理解してますか?

無駄にドリコムのスライドに釣られてませんか?

今回は Semicolonless Java について書いてみたいと思います。

Semicolonless Java

Semicolonless Java とはその名の通り、セミコロンを使わずに Java でプログラミングすることです。

ある種のパズル、コードゴルフの類に近いといった方が良いかも知れません。

簡単な例ですが、Hello world を出力するプログラムは以下のように書きます。

public class Hello {
        public static void main(String[] args) {
                if (null == System.out.printf("Hello world")){}
        }
}

まあこのようにセミコロンを回避しつつコードを書いていくのですが、正直遊びの範疇をでません。

では本当の Semicolonless Javaは実現できないのでしょうか?

近年では ES6, Golang などセミコロンを書かなくても良い言語をよく目にするようになりました。

確かにセミコロンを書く必要のない言語の方が煩わしくないし、見た目もスッキリして人気がありそうです。

Java もそのようにセミコロンをなくせばもっと人気がでそうです。

従来の Java のコードをほぼそのままにセミコロンレスを実現することはできないものなのでしょうか?

JSR 199: Java Compiler API

Java には Java Compiler をプログラムから操作する API があります。

JSR 199: Java Compiler API です。javax.tools パッケージに属しています。

簡単な例を挙げます。

App.java

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import javax.tools.JavaCompiler.CompilationTask;

public class App {

  public static void main(String[] args) throws Exception {
    final String sourceFile = "./helloworld.java";
    final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)) {

        List<File> sourceFileList = new ArrayList<File>();
        sourceFileList.add(new File(sourceFile));
        final Iterable<? extends JavaFileObject> compilationUnits =
            fileManager.getJavaFileObjectsFromFiles(sourceFileList);
        final CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
        final boolean result = task.call();
        if (result) {
            System.out.println("Compilation was successful");
        } else {
            System.out.println("Compilation failed");
        }
    }
  }
}

ToolProvider.getSystemJavaCompiler() から JavaCompiler を取得、FileManagerコンパイル対象を設定、 CompilerTask を実行してコンパイルするような流れになります。

さてこの API ではどれくらいの事が可能なのでしょうか?

主に操作するとすれば以下の 2 クラスだと思います。

javax.tools.JavaCompiler

javax.tools.JavaCompiler.CompilationTask

見てみるとあの API は AnnotaionProcessor を絡めることは出来るかも知れません。

ですがコンパイラーの挙動を変更するなどといったカスタマイズ要件に対しては貧弱すぎてほぼ何もできないように見えます。

やはりセミコロンレスを実現するには処理系そのものを改修しないといけないのでしょうか??

Real world Java Compiler API

javax.tools.* 公開されている API では何もカスタマイズのしようがないのは事実です。

ですが世の中にはこの Compiler API を実際に使用して様々なことをやっているプロダクトが多数存在します。

ではどうしているのでしょうか?実際に使用している例を元に見ていきたいと思います。

ErrorProneの場合

最近、導入されることが多くなってきてる ErrorProne ですが、この ErrorProne もこの API を使用しています。

ErrorProneコンパイル時に静的解析を行います。そのため、この API でカスタマイズした静的解析付きコンパイラーを提供します。

土台になる部分のコードは以下のようになっています。

BaseErrorProneJavaCompiler.java

  @Override
  public CompilationTask getTask(
      Writer out,
      JavaFileManager fileManager,
      DiagnosticListener<? super JavaFileObject> diagnosticListener,
      Iterable<String> options,
      Iterable<String> classes,
      Iterable<? extends JavaFileObject> compilationUnits) {
    ErrorProneOptions errorProneOptions = ErrorProneOptions.processArgs(options);
    List<String> remainingOptions = Arrays.asList(errorProneOptions.getRemainingArgs());
    ImmutableList<String> javacOpts = ImmutableList.copyOf(remainingOptions);
    javacOpts = defaultToLatestSupportedLanguageLevel(javacOpts);
    javacOpts = setCompilePolicyToByFile(javacOpts);
    final JavacTaskImpl task =
        (JavacTaskImpl)
            javacTool.getTask(
                out, fileManager, diagnosticListener, javacOpts, classes, compilationUnits);
    setupMessageBundle(task.getContext());
    RefactoringCollection[] refactoringCollection = {null};
    task.addTaskListener(
        createAnalyzer(errorProneOptions, task.getContext(), refactoringCollection));
    return new CompilationTask() {
      @Override
      public void setProcessors(Iterable<? extends Processor> processors) {
        task.setProcessors(processors);
      }

      @Override
      public void setLocale(Locale locale) {
        task.setLocale(locale);
      }

      @Override
      public Boolean call() {
        return wrapPotentialRefactoringCall(
            task.call(), new PrintWriter(out, true), refactoringCollection[0]);
      }
    };
  }

注目ポイントは以下の部分です。

    final JavacTaskImpl task =
        (JavacTaskImpl)
            javacTool.getTask(
                out, fileManager, diagnosticListener, javacOpts, classes, compilationUnits);

ハイ!きました! JavacTaskImpl

ということで実際にカスタマイズしようとするとあのAPIでは貧弱すぎなので cast して使用しているというのが現実です。

ErrorProneJavacTaskImplcast することで使える addTaskListener メソッドを使って静的解析を行っているのです。

簡単に addTaskListener メソッドの説明をしておくとコンパイラーは幾つかのステージを踏んでクラスファイルを生成するのですが、このメソッドはその各ステージ、成果物作成毎に呼ばれるコールバックを設定するメソッドです。

  • ステージ情報
  • 対象成果物

ステージ開始、終了時に特定のメソッドが呼ばれるので ErrorProne はその情報を元に解析を行っているのです。

その他にも JavacTask (JavacTaskImpl) は使われています。

ENSIMEJava の型情報の取得、補完情報なども JavacTask を使用して解析されています。

では JavacTaskImpl を使うとどれくらいのことができるのでしょうか?

セミコロンレスは実現できるのでしょうか??

Parser 載せ替え Hack

セミコロンレスを実現するにはシンプルに考えて AST を作成時にセミコロンの有無を無視してくれれば良さそうです。

AST を作成する Parser 実装を差し替えることができればなんとかなりそうですね。

ですが、Parser を載せ替えるには ParserFactory をなんらかの方法でコンパイラに読み込ませる必要があります。

この辺からは com.sun.tools.* の知識が必要となってきますが、みなさんご存知のことでしょうからあまり深くふれないことにします。

Context

コンパイラ部は他のクラスと情報をやりとりする際に Context クラスを使用しています。

このクラスは HashTable のクラスのような構造でコンパイラ部で使用するものが雑多に詰まっています。

この中には ParserFactory なども含まれています。

Contextコンパイラが実行する前に取得し、なんらかの方法で ContextParserFactory を載せることができれば…。

では早速チャレンジしてみましょう。

実装

当たり前ですが、Parser 部はすべて package-private になっており、外部からは修正できないようになっています。

そのため、プロジェクト内で com.sun.tools.javac.parser を定義し差し替えます。

SemicolonlessParser.java

package com.sun.tools.javac.parser;

import static com.sun.tools.javac.parser.Tokens.TokenKind.SEMI;

public class SemicolonlessParser extends JavacParser {

    protected SemicolonlessParser(final SemicolonlessParserFactory parserFactory,
                                  final Lexer lexer,
                                  final boolean b,
                                  final boolean b1,
                                  final boolean b2) {
        super(parserFactory, lexer, b, b1, b2);
    }

    @Override
    public void accept(final Tokens.TokenKind tk) {
        if (token.kind == tk) {
            nextToken();
        } else if (tk == SEMI) {
        } else {
            super.accept(tk);
        }
    }
}

とても雑な実装ですが、とりあえずセミコロン判定を無視していきます。

そしてこの Parser を適用するための ParserFactory を実装します。

SemicolonlessParserFactory.java

package com.sun.tools.javac.parser;

import com.sun.tools.javac.util.Context;

public class SemicolonlessParserFactory extends ParserFactory {

    protected SemicolonlessParserFactory(final Context context) {
        super(context);
    }

    public static SemicolonlessParserFactory instance(final Context context) {
        SemicolonlessParserFactory instance = (SemicolonlessParserFactory) context.get(parserFactoryKey);
        if (instance == null) {
            instance = new SemicolonlessParserFactory(context);
        }
        return instance;
    }

    @Override
    public JavacParser newParser(final CharSequence input,
                                 final boolean keepDocComments,
                                 final boolean keepEndPos,
                                 final boolean keepLineMap) {
        final Lexer lexer = scannerFactory.newScanner(input, keepDocComments);
        return new SemicolonlessParser(this, lexer, keepDocComments, keepLineMap, keepEndPos);
    }

}

重要な実装はこれぐらいです。

どうでしょうか?とてもコード量が少なくて楽勝な感じがしてきましたね。

ではこれを Context に設定して使用してもらうようにコンパイラ部を書いてみましょう。

BaseSemicolonlessCompiler.java

import com.sun.tools.javac.api.JavacTaskImpl;
import com.sun.tools.javac.parser.SemicolonlessParserFactory;
import com.sun.tools.javac.util.Context;

import javax.annotation.processing.Processor;
import javax.lang.model.SourceVersion;
import javax.tools.*;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.Locale;
import java.util.Set;

public class BaseSemicolonlessCompiler implements JavaCompiler{
    private final JavaCompiler javaCompiler;

    public BaseSemicolonlessCompiler(){
        this.javaCompiler = ToolProvider.getSystemJavaCompiler();
    }

    @Override
    public CompilationTask getTask(Writer out, JavaFileManager fileManager, DiagnosticListener<? super JavaFileObject> diagnosticListener, Iterable<String> options, Iterable<String> classes, Iterable<? extends JavaFileObject> compilationUnits) {
        final JavacTaskImpl task = (JavacTaskImpl) javaCompiler.getTask(out, fileManager, diagnosticListener, options, classes, compilationUnits);
        replaceParser(task);
        return new CompilationTask() {
            @Override
            public void setProcessors(Iterable<? extends Processor> processors) {
                task.setProcessors(processors);
            }

            @Override
            public void setLocale(Locale locale) {
                task.setLocale(locale);
            }

            @Override
            public Boolean call() {
                return task.call();
            }
        };
    }

    @Override
    public StandardJavaFileManager getStandardFileManager(DiagnosticListener<? super JavaFileObject> diagnosticListener, Locale locale, Charset charset) {
        return this.javaCompiler.getStandardFileManager(diagnosticListener, locale, charset);
    }

    @Override
    public int isSupportedOption(String option) {
        return 0;
    }

    @Override
    public int run(InputStream in, OutputStream out, OutputStream err, String... arguments) {
        return this.javaCompiler.run(in, out, err, arguments);
    }

    @Override
    public Set<SourceVersion> getSourceVersions() {
        return this.javaCompiler.getSourceVersions();
    }

    private void replaceParser(final JavacTaskImpl compilerTask) {
        final Context context = compilerTask.getContext();
        SemicolonlessParserFactory.instance(context);
    }

}

重要なのは replaceParser メソッドのみです。

JavacTaskImplcast すると getContext メソッドが見えるようになり、Context を操作することができます。

ParserFactory.instance にセットしてしまえば準備完了です。

では簡単に呼び出せるようにさらに実装をしていきましょう。

SemicolonlessCompiler.java

import javax.tools.*;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class SemicolonlessCompiler {

    public CompileResult compile(final List<File> sourceFileList, final File output) throws IOException {
        final JavaCompiler compiler = new BaseSemicolonlessCompiler();
        try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)) {
            final Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(sourceFileList);
            final DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
            final List<String> compileOptions = Arrays.asList(
                    "-g", "-deprecation",
                    "-d", output.getCanonicalPath(),
                    "-source", "1.8",
                    "-target", "1.8",
                    "-encoding", "UTF-8"
            );

            final JavaCompiler.CompilationTask task = compiler.getTask(null,
                    fileManager,
                    diagnosticCollector,
                    compileOptions,
                    null,
                    compilationUnits);
            final boolean result = task.call();
            return new CompileResult(result, diagnosticCollector.getDiagnostics());
        }
    }

    public class CompileResult {

        private final boolean success;
        private List<Diagnostic<? extends JavaFileObject>> diagnostics = new ArrayList<>();

        CompileResult(final boolean success,
                      final List<Diagnostic<? extends JavaFileObject>> diagnostics) {
            this.success = success;
            this.diagnostics = new ArrayList<>(diagnostics);
        }

        public boolean isSuccess() {
            return success;
        }

        public List<Diagnostic<? extends JavaFileObject>> getDiagnostics() {
            return diagnostics;
        }
    }
}

動作確認にための簡単なテストを書いて実行してみましょう。

SemicolonlessCompilerTest.java

import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.*;

public class SemicolonlessCompilerTest {


    @Test
    public void testCompile() throws IOException {

        final File out = new File(System.getProperty("java.io.tmpdir"), "out");
        out.mkdirs();
        final SemicolonlessCompiler compiler = new SemicolonlessCompiler();
        final List<File> targets = new ArrayList<>();
        final File file = new File("./src/test/resources/Hello.java").getCanonicalFile();
        assertTrue(file.exists());
        targets.add(file);
        final SemicolonlessCompiler.CompileResult compileResult = compiler.compile(targets, out);
        assertEquals(true, compileResult.isSuccess());
    }
}

コンパイル対象は以下です。もちろんセミコロンレスです。

Hello.java

public class Hello {

    public void greeting() {
        System.out.println("Hello world")
    }

    public static void main(String[] args) throws Exception {
        new Hello().greeting()
    }

}

テストを実行すると tmpdiroutHello.class ができているはずです。

そのディレクトリへ移動し、実行してみると…。

$ java Hello
Hello world

成功しました!

これでいくらでもセミコロンレスにできそうです!!!

とざっくり Java コンパイラーの挙動を変える方法を紹介してみました。

今回、作成したものは以下から参照できます。興味のある方は見て動かしてみて下さい。

github.com

Abby 社員募集について

最後に現在 株式会社Abby では社員を募集しています。

現在進行系のプロジェクトでは以下のような技術を使用しています。(主なもの)

  • Java + libGDX でのクライアント開発
  • Golang でのサーバーサイド開発
  • Vue.js を使った SPA 開発
  • React Native を使ったユーティリティツールの開発(予定)

興味がある方は info@abby.co.jp までメールを下さい。

もしくはTwitterFacebookで@mopemope宛まで連絡してください。

そして一度弊社まで遊びにきてください。皆様からの応募お待ちしております。

(あのeseharaでさえ一度遊びに来てるので多くの場合問題ないと思います)

あと EmacsJava 書きたい狂人のコントリビューターも募集中です!!

なお、東方神起のメンバーは募集していませんのでご了承ください。