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.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
して使用しているというのが現実です。
ErrorProne
は JavacTaskImpl
へ cast
することで使える addTaskListener
メソッドを使って静的解析を行っているのです。
簡単に addTaskListener
メソッドの説明をしておくとコンパイラーは幾つかのステージを踏んでクラスファイルを生成するのですが、このメソッドはその各ステージ、成果物作成毎に呼ばれるコールバックを設定するメソッドです。
- ステージ情報
- 対象成果物
ステージ開始、終了時に特定のメソッドが呼ばれるので ErrorProne
はその情報を元に解析を行っているのです。
その他にも JavacTask
(JavacTaskImpl
) は使われています。
ENSIME
の Java
の型情報の取得、補完情報なども JavacTask
を使用して解析されています。
では JavacTaskImpl
を使うとどれくらいのことができるのでしょうか?
セミコロンレスは実現できるのでしょうか??
Parser 載せ替え Hack
セミコロンレスを実現するにはシンプルに考えて AST
を作成時にセミコロンの有無を無視してくれれば良さそうです。
AST
を作成する Parser
実装を差し替えることができればなんとかなりそうですね。
ですが、Parser
を載せ替えるには ParserFactory
をなんらかの方法でコンパイラに読み込ませる必要があります。
この辺からは com.sun.tools.*
の知識が必要となってきますが、みなさんご存知のことでしょうからあまり深くふれないことにします。
Context
コンパイラ部は他のクラスと情報をやりとりする際に Context
クラスを使用しています。
このクラスは HashTable
のクラスのような構造でコンパイラ部で使用するものが雑多に詰まっています。
この中には ParserFactory
なども含まれています。
Context
をコンパイラが実行する前に取得し、なんらかの方法で Context
に ParserFactory
を載せることができれば…。
では早速チャレンジしてみましょう。
実装
当たり前ですが、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
メソッドのみです。
JavacTaskImpl
に cast
すると 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() } }
テストを実行すると tmpdir
の out
に Hello.class
ができているはずです。
そのディレクトリへ移動し、実行してみると…。
$ java Hello Hello world
成功しました!
これでいくらでもセミコロンレスにできそうです!!!
とざっくり Java コンパイラーの挙動を変える方法を紹介してみました。
今回、作成したものは以下から参照できます。興味のある方は見て動かしてみて下さい。
Abby 社員募集について
最後に現在 株式会社Abby では社員を募集しています。
現在進行系のプロジェクトでは以下のような技術を使用しています。(主なもの)
興味がある方は info@abby.co.jp までメールを下さい。
もしくはTwitterやFacebookで@mopemope宛まで連絡してください。
そして一度弊社まで遊びにきてください。皆様からの応募お待ちしております。
(あのeseharaでさえ一度遊びに来てるので多くの場合問題ないと思います)
あと Emacs
で Java
書きたい狂人のコントリビューターも募集中です!!
なお、東方神起のメンバーは募集していませんのでご了承ください。