Javaソースコードからコメントを除去した文字列を取得する

Javaソースコードのimportの解析をしたいので、まずJavaソースコードからコメントを除去することを考えた。
おそらくJavaソースコードの解析をするシステムはいくらでもあるものと思われるが、あまりに大掛かりなものは避けたい。

Removing all the comments in a Java source file.

RegExで行う方法

調べてみると、Javaコメントを削除するRegExとしていくつか提案されている。

しかし、これらはうまく行かない。例えばブロックコメントの開始(/*)が文字列の中にあったり、ラインコメントの中にある場合に誤動作してしまう。例えば、以下のような極端なコードでは動作しない。

Above solutions don’t work on the following weired source.

import/**/java.io.*; /*
*/import java.util.*;
// /* ブロックコメントではない
public class CommentRemoverCommentTest {
  /** フィールド **/
  private String a = "\"sample // string";
  private char b = '"';
  private String c = "/* 'sample /* string";  
  public CommentRemoverCommentTest() {
    /** comment **/
    // TODO Auto-generated constructor stub
  }
}

自作のモジュール

そこで自作モジュールを作成することにした。以下のものだ。単純にCommentRemover.remove()を対象のJavaソースについて呼び出せばよい。

I’ve written the following solution. Just call CommentRemover.remove() on your Java source file.


import java.io.* ; import java.nio.charset.*; import java.nio.file.*; import java.util.stream.*; /** * Javaソースからコメントを削除した文字列を取得する。 */ public class CommentRemover { /** Javaソースのエンコーディング */ public static final Charset CHARSET= Charset.forName("UTF-8"); /** * 指定Javaソース・ファイルを読み込み、コメントを除去した後のテキストを返す * ブロックコメントの場合、改行が含まれない場合は一つの半角スペースに、含まれる場合には一つの改行に置換する。 * @param path 対象とするファイル * @return コメント除去した後のファイル文字列。改行コードは"\n" * @throws IOException */ public static String remove(Path path) throws IOException { String text = Files.readAllLines(path, CHARSET).stream().collect(Collectors.joining("\n")); StringBuffer stripped = new StringBuffer(); CharReader r = new CharReader(text); while (!r.endOfFile()) { char c = r.read(); switch (c) { case '"': // 文字列定数開始 case '\'': // 文字定数開始 stripped.append(skipStringOrChar(c, r)); break; case '/': // コメント開始の可能性 switch (r.read()) { case '*': // ブロックコメント開始 stripped.append(skipBlockComment(r)); break; case '/': // 行コメント開始 stripped.append(skipLineComment(r)); break; default: // コメントではない stripped.append(c); break; } break; default: // 注目しない文字 stripped.append(c); continue; } } return stripped.toString(); } /** * Javaのブロックコメントをスキップする。 * コメント途中に改行の無い場合には空白一文字に置き換える。 * 改行のある場合には改行一文字に置き換える。 * @param r * @return */ static String skipBlockComment(CharReader r) throws IOException { boolean hasNewLine = false; while (true) { char c = r.read(); if (c == '*') { c = r.read(); if (c == '/') break; r.unread(); continue; } if (c == '\n') { hasNewLine = true; continue; } } return hasNewLine? "\n":" "; } /** * Javaのラインコメントをスキップする * @param r * @return */ static String skipLineComment(CharReader r) throws IOException { while (!r.endOfFile()) { if (r.read() == '\n') return "\n"; } return ""; } /** * 文字列あるいは文字定数をスキップする * * @param start 開始文字、'"'あるいは'\'' * @param r リーダ * @return 文字列あるいは文字定数 * @throws IOException */ static String skipStringOrChar(char start, CharReader r) throws IOException { StringBuilder output = new StringBuilder(); output.append(start); while (true) { if (r.endOfFile()) break; char c = r.read(); if (c == start) { output.append(c); break; } output.append(c); if (c == '\\') output.append(r.read()); } return output.toString(); } /** * 文字リーダ * {@link #endOfFile()}でファイルの終わりを検出する。 * {@link #read()}で一文字を返すが、ファイル終端では{@link IOException}が発生する。 */ static class CharReader { BufferedReader r; CharReader(String text) { r = new BufferedReader(new StringReader(text)); } boolean endOfFile() throws IOException { r.mark(1); int c = r.read(); if (c < 0) return true; r.reset(); return false; } char read() throws IOException { r.mark(1); int c = r.read(); if (c < 0) throw new IOException("Truncated source file"); return (char)c; } void unread() throws IOException { r.reset(); } String getRest() throws IOException { StringBuilder s = new StringBuilder(); while (!endOfFile()) s.append(read()); return s.toString(); } } }

これで先の対象ファイルを処理すると以下の出力になる。

The output of that weired source file will be the following.

import java.io.*; 
import java.util.*;

public class CommentRemoverCommentTest {

  private String a = "\"sample // string";
  private char b = '"';
  private String c = "/* 'sample /* string";  
  public CommentRemoverCommentTest() {


  }
}