Java/EclipseでDSLサポート (2) - コンパイルプロセスへの介入
次に、Eclipse JDTの拡張ポイントを利用してコンパイルプロセスに介入していきます。
調べながら同時に書いていくのでゆっくりと。
方針
全体像を示しておくと、こんな感じです。
- DSLが確実にコンパイルできない場合はエラーを表示する
- DSLが静的に解析可能で、エラーであることが自明である場合
- DSLがコンパイルできない可能性がある場合は警告を表示する
- DSLが挿入されるべき場所に静的に解析できない変数などが含まれている場合
これらを表示するタイミングは、EclipseのJDTと同じようにしたいと思います。つまり
- コーディングの最中にエラーや警告を表示
- 保存(ビルド)時にエラーや警告があれば、Problemsビューにそれらを表示
といった感じです。
いろいろと調べてみたところ、警告やエラー表示はそれなりに簡単にできる模様で、調べながら物を作っていく過程を紹介していきたいと思います。
今回分の方針は、コンパイルプロセスへの介入をどうやればいいのかを調べるため、かなり簡素化したつくりにします。
プラグインの作成
マーカーの作成
Eclipseに警告やエラーを表示させる場合、「org.eclipse.core.resources.markers」という拡張ポイントを使ってマーカーと呼ばれるものを作成し、それをエディタに貼り付ける形になります。今回作るマーカーは「com.example.ashigeru.marker」という名前で、次のような記述にします。
<extension id="com.example.ashigeru.marker" point="org.eclipse.core.resources.markers"> <persistent value="true"> </persistent> <!-- Problemsビューに表示させる --> <super type="org.eclipse.core.resources.problemmarker"> </super> <!-- テキストエディタ特有 --> <super type="org.eclipse.core.resources.textmarker"> </super> </extension>
マーカーは特に実装クラスなどが必要ではありませんが、JDTで使う場合には"org.eclipse.jdt.core.compiler.CategorizedProblem"というクラスとマッピングさせておくといろいろな場面で利用できます。
ということで、MyProblem (extends CategorizedProblem)というクラスを作って、先ほど作成したマーカーとマッピングさせておきます。
package com.example.ashigeru; import org.eclipse.core.runtime.IStatus; import org.eclipse.jdt.core.compiler.CategorizedProblem; import org.eclipse.jdt.core.compiler.IProblem; public class MyProblem extends CategorizedProblem { /** extension id とそろえる。 */ public static final String MARKER_ID = "com.example.ashigeru.marker"; private int status; private String message; private String fileName; private int sourceStart; private int sourceEnd; private int lineNumber; public MyProblem(int status, String message, String fileName) { super(); this.status = status; this.message = message; this.fileName = fileName; } @Override public int getCategoryID() { return CategorizedProblem.CAT_UNSPECIFIED; } @Override public String getMarkerType() { return MARKER_ID; } public String[] getArguments() { return new String[0]; } public int getID() { return IProblem.ExternalProblemNotFixable; } public String getMessage() { return message; } public char[] getOriginatingFileName() { return fileName.toCharArray(); } public boolean isError() { return status == IStatus.ERROR; } public boolean isWarning() { return status == IStatus.WARNING; } public int getSourceStart() { return sourceStart; } public int getSourceEnd() { return sourceEnd; } public int getSourceLineNumber() { return lineNumber; } public void setSourceStart(int sourceStart) { this.sourceStart = sourceStart; } public void setSourceEnd(int sourceEnd) { this.sourceEnd = sourceEnd; } public void setSourceLineNumber(int lineNumber) { this.lineNumber = lineNumber; } }
コンパイルプロセスへの介入
Eclipse3.2くらいから、Eclipse JDTにAPTの機能が組み込まれました。こいつの実装は、JDTのコンパイルプロセスに介入しているのですが、そのタイミングからorg.eclipse.jdt.coreプラグインにcompilationParticipantという拡張ポイントが整備され、これを利用してコンパイルプロセスへの介入を簡単にできるようになりました。
とりあえず、拡張ポイントをさらしておきます。
<extension id="com.example.ashigeru.compilationParticipant" point="org.eclipse.jdt.core.compilationParticipant"> <!- com.example.ashigeru.MyCompilationParticipantをつかう。Problemsを生成 -> <compilationParticipant class="com.example.ashigeru.MyCompilationParticipant" createsProblems="true" id="com.example.ashigeru.myCompilationParcipant" modifiesEnvironment="false"> <!- 先ほど作成したマーカーcom.example.ashigeru.markerを使う -> <managedMarker markerType="com.example.ashigeru.marker"> </managedMarker> </compilationParticipant> </extension>
この拡張ポイントでは、「org.eclipse.jdt.core.compiler.CompilationParticipant」というコンパイラに介入する処理をJDTにぶち込むことができます。こいつは主にAPTがうまく行くように作られており、
- ソースコードを書いている最中にreconcile要求がかかると、書いている途中のソースコードに警告やエラーを出せる (CompilationParticipant#reconcile)
- ビルド処理に介入できる (CompilationParticipant#buildStarting)
といったことができます。
上の記述では次のような制約の元に、コンパイルプロセスでいろんなことをやってOK。
- createsProblems="true" : 警告やエラーを表示できる
- modifiesEnvironment="false" : 環境に影響を与えられない
- markerType="com.example.ashigeru.marker" : MyProblem ("com.example.ashigeru.marker") を使える
ということで、MyCompilationParticipantはこんな感じになりました。
package com.example.ashigeru; import java.util.ArrayList; import java.util.List; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.compiler.BuildContext; import org.eclipse.jdt.core.compiler.CompilationParticipant; import org.eclipse.jdt.core.compiler.ReconcileContext; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.StringLiteral; public class MyCompilationParticipant extends CompilationParticipant { @Override public boolean isActive(IJavaProject project) { return true; } // 書いている途中の警告表示 @Override public void reconcile(ReconcileContext context) { CompilationUnit ast; try { ast = context.getAST3(); if (ast == null) { return; } } catch (JavaModelException e) { return; } String fileName = context.getWorkingCopy().getElementName(); List<MyProblem> problems = createProblems(fileName, ast); context.putProblems(MyProblem.MARKER_ID, problems.toArray(new MyProblem[problems.size()])); } // ビルド時の警告表示 @Override public void buildStarting(BuildContext[] files, boolean isBatch) { for (BuildContext context : files) { String fileNAme = context.getFile().getName(); CompilationUnit ast = createAstFromFile(context.getFile()); List<MyProblem> problems = createProblems(fileNAme, ast); context.recordNewProblems( problems.toArray(new MyProblem[problems.size()])); } } // IFileからASTを作る private CompilationUnit createAstFromFile(IFile file) { ICompilationUnit cu = JavaCore.createCompilationUnitFrom(file); ASTParser parser = ASTParser.newParser(AST.JLS3); parser.setSource(cu); parser.setResolveBindings(true); CompilationUnit ast = (CompilationUnit) parser .createAST(new NullProgressMonitor()); return ast; } // ASTから文字列リテラルを探し出して、全部に警告を出す private List<MyProblem> createProblems(final String fileName, final CompilationUnit ast) { final List<MyProblem> problems = new ArrayList<MyProblem>(); // すべてのStringLiteralに対して警告を出しておく ast.accept(new ASTVisitor() { @Override public boolean visit(StringLiteral node) { MyProblem prom = new MyProblem(IStatus.WARNING, node .getEscapedValue(), fileName); int start = node.getStartPosition(); prom.setSourceStart(start); prom.setSourceEnd(start + node.getLength()); prom.setSourceLineNumber(ast.getLineNumber(start)); problems.add(prom); return false; } }); return problems; } }
実行してみるとこんな感じ。
次は「すべての文字列リテラル」から「DSLとして利用される文字列リテラル」に範囲を絞って警告やエラーを出そうと思います。