メンバページ: Go

汎用メトリクスツール「いちゃもん」の作り方 by Java

General-Purpose Metrics Tool "Fuss" making by Java


このページでは、C/C++/C#/Java に対応する汎用的なメトリクスツールを作成していきます。

- 更新記録 -
1日目 メトリクスとは
2日目 クラス設計 最初の始まり
3日目 ファイルフィルタとワイルドカード
4日目 字句解析部のインポート
5日目 シンボルテーブルなどの部品クラスの作成
6日目 プログラム解析・管理部の作成
7日目 メトリクスデータのファイル出力
8日目 まとめと結果のサンプル
参照 Ichamon のその後はここにあります

1日目 --- メトリクスとは

今日はメトリクスについて記述します。ここでメトリクスとは、プログラムを何らかの計測基準で計量(メジャー)することを指します。

例えば、「行数」でプログラムを計測する方法があります。システム全体は何行であるとか、1ファイル何行であるとか、1クラス平均何行であるかなどを計測します。

この計測の基準となるもの、つまり物差しに各種のものが提案されています。以下に代表的なメトリクスを紹介します。

  1. LOC (Line Of Codes、行数、ステップ数)
    プログラムの行数。正確にはプログラム中の改行の個数に EOF のみでない EOF がある行を含む個数である。
    この LOC には、空白行やコメント行、宣言部などの非実行文も含まれる。
  2. 命令行数
    プログラムの命令が含まれる行の個数。コメント文や空行を除いたもので、宣言文やマクロ指定行なども命令行数に入れることが多い。
  3. ステートメント数
    プログラム言語の構文規則で、ステートメント(文)として認識されるプログラム断片の個数。
    例えば、for (int i = 0; i < size; i ++) は、(1) i = 0, (2) i < size, (3) i ++ の 3 ステートメントである。
  4. メッセージ送信数、関数コール数
    +, *, < などの演算子による演算を含まない、メッセージ送信の個数である。
    例えば、"abc" + "def" は 0 であるが、str.append("def") は 1 個である。(これは矛盾の例となっている)
  5. 演算子とオペランドの個数
    ここにはメッセージ送信も含まれる。
    1 + 2 は1個の演算子に2個のオペランド、if (a == b) x = a; else x = b; は 4 個の演算子(if-else, ==, =x2)、6 個のオペランド(a, b, x, a, x, b)である。 
  6. ユーザ定義関数数
    ユーザ定義パッケージ、クラス、メソッド、関数、ファイルなどの個数やその段数
  7. 使用システム関数数
    使用しているシステムのパッケージ、クラス、メソッド、関数、ファイルなどの個数やその段数
  8. 変数数
    変数(グローバル変数、ローカル変数、クラス内グローバル変数、・・・)の個数やサイズの合計値
  9. サイクロマティック数
    プログラムの制御構造をグラフとしてみたときの独立閉路数(いわゆるサイクロマティック数)。
  10. それぞれの平均値と分散

上記が代表的なもので、この他のメトリクスも存在します。

 

これらのメトリクスはプログラムを直接計測することにより、その値が得られます。 これを一次メトリクスと呼んでいます。

この一次メトリクスを使って計算して、二次メトリクスを得ることができます。多くのメトリクスツールではこの二次メトリクスを計算して、プログラムの総合的な計測結果を与えるようにしています。

「いちゃもん」では上記のうちの基本的なメトリクスを計測するようにします。少なくとも 1 から 3、6、 7 を計測するようにします。

またプログラムに「いちゃもん」をつけるために上記のものに加えて、以下のものも計測するようにします。

  1. 1行の文字数(長すぎる行を計測する)
  2. シンボルの文字数(長すぎる名前を計測する)

以下に現時点で想定している一次メトリクスの結果を例示します。

トータル
  行数                      128
  命令行数                   96
  ステートメント数           64
  コメント行数               32
  空行                       16
  クラス数                    2
  メソッド数                  4
  使用システムメソッド数      4

ファイル         test.Test.java 
  行数                      128
  命令行数                   96
  ステートメント数           64
  コメント行数               32
  空行                       16
  クラス数                    2
  メソッド数                  4
  使用システムメソッド数      4
  クラス                   Test
    メソッド               main
      出現行                  7
      ステートメント数       32
    メソッド              print
      出現行                 48
      ステートメント数       16

また一次メトリクスで計測する警告データの表示として、以下のものを考えています。

                             ファイル     行番号
80桁を越える行
16文字を越える名前           

 

明日は、作成方針を軽く立てて、概要レベルのクラス設計をしてみます。

See you again !!

このページトップへ

2日目 --- クラス設計 最初の始まり --- 現物系クラスの設計

今日は最初のクラス設計をします。ここで「最初」と言っているには、クラス設計で2回目以降があるということを匂わせています。今日は最初のクラス設計の始まりを書いていきます。

クラス候補

まずは、システム設計の前にシステム分析をします。このメトリクスツールが行うことを時間の流れにそって記述してみましょう。

  1. ソースプログラムを読み込む
      ソースプログラムの記述子を解析し、ファイルパスのグループを得る
      ファイルパスのグループに対応する複数のファイルを読み込む
  2. 読み込んだソースプログラムを字句解析・構文解析する
      字句解析する
      構文解析する、但しここでは簡単な構文解析でよい
  3. 注目するデータを抽出する
      データを収集する
      データに統計的処理を行う
  4. その結果をファイルや画面に出力する
      データを見やすいように変形する
      HTML, CSV 形式にしてファイル書き込みを行う

ファイル読み込み、構文解析、データ抽出、ファイル出力の4つの大きな流れがあります。これが機能クラス(またはコントロールクラスと呼ぶ)の候補になります。

次にこのシステムで登場する主要な登場人物、つまりモデルとなるオブジェクトを以下に示します。

  1. ファイル群
  2. ファイル
  3. ソースプログラム
  4. 語(トークン)
  5. クラス
  6. メソッド、関数
  7. ステートメント
  8. メトリクスデータ
  9. 一次メトリクスデータ (平均ファイルステートメント数、平均クラスステートメント数など)
  10. 二次メトリクスデータ各種の統計データ()
  11. 出力ファイル
  12. プログラム解析の状態(解析ファイル名、解析行、クラスの中か、メソッドの中かなど)

これらがクラスオブジェクトやインスタンスオブジェクトの候補、またはそれぞれのオブジェクトの属性の候補になります。

最後にこのシステムの外部インタフェースを挙げてみます。

  1. 1個または複数のソースファイルを入力として、そのメトリクスデータをファイル出力する

これは一般には API (アプリケーションインタフェース)と呼ばれているものです。 Java のインタフェースと区別するために境界(バウンダリ)系と呼ぶことにします。これもクラス候補の一つになります。

以上より、機能系で4個、モデル系で13個、境界系で1個のクラス候補を発見したことになります。

後は

  1. 特化方向の共通化のための部品クラス
  2. 一般化方向の共通化のための抽象化クラス
  3. イベント系クラス・例外系クラスなどその他の特定用途のクラス

などの現物系クラスでない潜在的なクラスの発見が必要になります。

現物系クラスの設計

まずは、機能系、モデル系、境界系(これは現物系ではありませんが、ついでにクラスを作成します)の現物系クラスの設計をしてみます。

前節で書いたクラス発見の第一のフィルタとしては、そのオブジェクトの「サイズ」に注目するといいでしょう。

やはり、クラスは即値型と比較して効率が悪いものなので、一定のサイズ以上のものをクラスにするのがいいでしょう。

このサイズの閾値は人(の趣味)やプロジェクトにより異なります。 Java には構造体がありませんので、構造体とクラスの両方がある言語に比較して、その閾値は小さくなるでしょう。 その閾値は、例えば8個では既に即値型として扱うのには多すぎるでしょう。4個であれば迷うでしょう。1個であれば、クラスにするにはサイズ面からは大げさすぎるでしょう。

このサイズだけの観点で、モデル系を見ますと、以下のものが選定したクラス候補になります。

  1. ファイル群(ファイルのコレクションクラス)
      属性には、ファイルを要素とするコレクションデータがある
  2. 語(トークン)群
      属性には、システムのトークンかユーザ定義のトークンか、関数か変数かなどの種別がある
  3. ファイル単位のメトリクスデータ
      ファイル単位の行数、ステートメント数、クラス数などのメトリクスデータがある。
  4. プログラム断片(クラスやメソッド)単位のメトリクスデータ
      メソッド単位の行数、ステートメント数などのメトリクスデータがある。
  5. プログラム解析の状態
      括弧やカーリーブラケットの深さ、メソッドの内外、クラスの内外などのプログラムの各種状態を保持する

4 のプログラム断片単位のメトリクスデータ と 5 のプログラム解析の状態は 扱う単位が両者とも同じプログラム断片になりますので、まずは同じクラスにしましょう。 大きくなるようであれば、将来のリファクタリングで分割します。

それぞれに「いい」名前を付けてクラスにしましょう(これは重要です)。 ここでは FileGroup, SymbolTable, FileStatus, ProgramStatus にしましょう。

注意: Data や Information, Flag の名前に情報は少ないのと同様に Status にもそれほど多くの情報はありませんが、それらよりはまだいいということで名前を付けています。

次に機能系を考えみましょう。そもそも機能系クラスは存在悪なのでしょうか。それとも必要悪なのでしょうか。はたまた正義なのでしょうか。

ここでは必要悪だと考えて、必要最低限の機能クラスを作成します。

  1. 読み込みと解析(ファイル読み込み+字句解析+簡単な構文解析)
  2. ファイル出力

ここもいい名前を付けましょう。ここでは ReaderPrinter にします。

最後に境界系は、

  1. 1個または複数のソースファイルを入力として、そのメトリクスデータをファイル出力する

があります。これは外部インタフェースとなりますので、「特に」いい名前を付けましょう。ここでは Ichamon にします。

まとめ

今日はクラス設計の最初の始まりとして、現物系のクラスを発見・抽出しました。結果として以下のクラスがありました。

  1. FileGroup
      ファイル群。入力ファイルの集合となるコレクションクラス。
  2. SymbolTable
      プログラム中に登場するトークンを管理するクラス。
  3. FileStatus
      ファイル単位のメトリクスデータを管理するクラス。
  4. ProgramStatus
      プログラム断片(クラス、メソッド)単位のメトリクスデータやそのプログラム断片の状態を保持する。
      将来は分割する可能性もある。
  5. Reader
      ファイル読み込みと字句解析、簡単な構文解析を行なう機能系クラス。
  6. Printer
      ファイル出力を行なう機能形系クラス。
  7. Ichamon
      外部 API となるクラス。スタートクラスの役目も持つ。

次回はクラス設計から離れて、最初のクラス実装として、ファイル群 FileGroup に関して書いていきます。ここではファイルフィルタが登場します。

See you again !!

このページトップへ

3日目 --- ファイルフィルタとワイルドカード --- ファイルグループクラスの作成

今日はファイルグループのクラスを作成していきます。このクラスは、ワイルドカード付きのファイル名を与えて、実際のファイル名を得るためのクラスです。例えば、ディレクトリに、"Abc.java", "Abc.class", "Abc$def.class" があるときは、以下のようになります。

  1. A*.java     --->  Abc.java
  2. *.*           --->  Abc.java, Abc.class
  3. [A-z|.|$]*   ---> Abc.java, Abc.class, Abc$def.class

ここで注意することは、1, 2 と 3 は異なる種類のワイルドカードを使っていることです。1, 2 はファイルアクセスでよく見かける種類のワイルドカードになっています。一方、3 はどちらかというと正式なワイルドカードになっています。つまり、BNF 記法などで使われている記法と同じ記法になっています。 能力的には 3 の方が大きく、1,2 は 3のサブセットを表現しています。ここでは 1,2 だけでなく 3 もサポートすることにします。

ワイルドカード処理のためにファイルフィルタを以下に定義します。

/**
 * Regular Expression for FileFilter
 */
class FileFilterImpl implements FileFilter {
  String regexp;						// Regular expression
  /**
   * File Filter implementor
   * @param regexp Reguler expression for matching file name
   */
  FileFilterImpl(String regexp) {
    this.regexp = regexp;
  }
  /**
   * checks accepting given refuler expression and file name
   * @param file For matching file 
   * @return Matching result
   */
  public boolean accept(File file) {
    try {
      Pattern pattern = Pattern.compile(regexp);
      Matcher match = pattern.matcher(file.getName());				
      return match.matches();
    } catch (Exception e) {
      return true;
    }
  }
}

ファイルフィルタを生成するとき(new FileFilter(regexp))に、正規表現式をコンパイル(pattern.compile(regexp))せずに、ワイルドカードの文字列をそのまま保持するようにします。

この正規表現式に対して、ファイル名がアクセプトされるかどうかをチェック(match.matches)します。ワイルドカードとして正しくないときは無制限に受け入れる(catch (Exception e) { return true; })ようにします。

デフォルトのワイルドカードの取り扱い(*.java や *.* など)よりも豊富な機能を提供することができるようになります。また $ が入るファイルなどは発見できませんでしたが、上記のものでは、例えば [A-z|$|.]* のように指定することにより、発見できます。

次にこのファイルフィルタを使う側を作成します。

/**
 * get File Name
 * @param name file name
 */
private void getFileName(String input) {
  String dir;			// directory
  if (input == "") return;
  int dirPos = input.lastIndexOf('/'); 
  if (dirPos >= 0) {
     dir = input.substring(0, dirPos);
  } else { 
     dirPos = input.lastIndexOf('\\'); 
     if (dirPos >= 0) {
        dir = input.substring(0, dirPos);
     } else { 
        dir = ".";
     }
  }
  File directory = new File(dir);
  String regexp = input.substring(dirPos + 1, input.length());
		
  // expand directy or file name with wildcard to file names
  File[] filesFile = directory.listFiles(new FileFilterImpl(regexp));
  if (filesFile == null) return;
  for (int j = 0; j < filesFile.length; j++) {
    String fileName = dir + "/" + filesFile[j].getName();
    files.add(fileName);
  }
  return;
}

このプログラムの前半部は、与えられたパス名から、ディレクトリとファイルの切り分けを行なっています。/ (スラッシュ)と \(バックスラッシュ)のどちらでも対応するようにしています。

 File[] filesFile = directory.listFiles(new FileFilterImpl(regexp)) でファイルフィルタを実行するようにしています。

その後は、得られたファイル名をディレクトリを気にせずにフラットにアレイリストに格納(files.add(fileName))しています。

以下は残りのプログラムになります。宣言やクラス定義、コンストラクタを作成しています。

/*
 * FileGroup
 * @author go
 */
package jp.co.okisoft.esc.metrics;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.regex.*;
import java.io.*;

/**
 * generates Source Program Files 
 */
public class FileGroup {
	/** file name list  */
	public ArrayList files = new ArrayList();
	
	/**
	 * generates File Name Group
	 * @param inputs File name group or directory, file name with wildcard
	 */
	FileGroup (String[] inputs){
		for (int i = 0; i < inputs.length; i++) {
			getFileName(inputs[i]);
		}
	}

また、各クラスに、オブジェクトのプリントメソッド print やクラス単位の単体テストを行なうメソッド test を定義することにします。このメソッドを呼び出すメインメソッドも以下に定義します。

     /**
	 * print
	 * @param out PrintStream for print
	 */
	public void print (PrintStream out) {
		out.println("Files:");
		Iterator elements = files.iterator();
		while (elements.hasNext()) {
			out.println((String) elements.next());
		}
	}
	
	/**
	 * for Test
	 * @author go
	 */
	public static void test (String[] args) {
		System.out.println("\nTEST:::FileGroup 1 (argument)");
		FileGroup fileGroup = new FileGroup(args);
		fileGroup.print(System.out);

		System.out.println("\nTEST:::FileGroup 2 (2 wild card .../[A-z|.|$]*)");
		String[] params1 = {"jp/co/okisoft/esc/metrics/[A-z|$|.]*", "[A-z|.]*"};
		FileGroup fileGroup1 = new FileGroup(params1);
		fileGroup1.print(System.out);

		System.out.println("\nTEST:::FileGroup 3 (null string)");
		String[] params2 = {""};
		FileGroup fileGroup2 = new FileGroup(params2);
		fileGroup2.print(System.out);

		System.out.println("\nTEST:::FileGroup 4 (illegal string)");
		String[] params3 = {"*"};
		FileGroup fileGroup3 = new FileGroup(params3);
		fileGroup3.print(System.out);
	}

	/**
	 * dummy main
	 * @param args nothing
	 */
	public static void main (String[] args) {
		test(args);
	}

}

実行結果は以下のようになります。

TEST:::FileGroup 1 (argument)
Files:
jp/co/okisoft/esc/metrics/FileGroup.class
jp/co/okisoft/esc/metrics/FileGroup.java
./.classpath
./.project
./jp

TEST:::FileGroup 2 (2 wild card .../[A-z|.|$]*)
Files:
jp/co/okisoft/esc/metrics/FileGroup$FileFilterImpl.class
jp/co/okisoft/esc/metrics/FileGroup.class
jp/co/okisoft/esc/metrics/FileGroup.java
./.classpath
./.project
./jp

TEST:::FileGroup 3 (null string)
Files:

TEST:::FileGroup 4 (illegal string)
Files:
./.classpath
./.project
./jp

Test 1 は引数として、jp/co/okisoft/esc/metrics/*.* *.* を与えています。$付きのものはこれでは発見できません。Test 2 ではjp/co/okisoft/esc/metrics/[A-z|$|.]* [A-z|.]* を与えています。$ 付きのファイル名も発見できています。Test 3では "" (ナルストリング)を与えています。結果はありません。Test 4 ではワイルドカードとしては誤りの "*"を単独で与えています。この場合はすべてのファイルを返すようにしています。

 

今日はファイルフィルタを使ってファイル指定でワイルドカードを扱えるようにしました。明日は「やさしい Lisp の作り方」で作成した字句解析部を持ってきてプラグインするようにしてみます。

See you again !!

このページトップへ

4日目 --- 字句解析部のインポート

今日は、字句解析の骨格を作ります。以前作成した「やさしい Lisp の作り方」の字句解析部の骨格を持ってきて、変更して使うようにします。

1ファイルの読み込み readFile と1行読み込み getLine、1文字読み込みの getChar、1文字先読みの nextChar を作成します。

少し長いですが、そのまま紹介します。

/**
 * read source program file and get function names
 * @param fileName source program file name 
 */
public void readFile(String fileName) {
  this.fileName = fileName;
  status.registFileName(fileName);
  try {
    file = new FileInputStream(fileName);
  } catch (FileNotFoundException e) {
    println("File " + fileName + " is NOT FOUND by Reader.");
    return;
  }
  in = new InputStreamReader(file);	
  br = new BufferedReader(in);         // generats Buffer Reader
  EOF = false;                         // EOF is false
  getLine();                           // get 1 line from file 
  getChar();                           // get 1 character;
  try {
    getTokens();                       // get tokens 
  } catch (Exception e) {
    e.printStackTrace();
  }
  try {
    br.close();
    in.close();
    file.close();
    status.saveStatus();
  } catch (IOException e) {
    println("File " + fileName + " is NOT CLOSE.");
    return;			
  }
}

/** get 1 Line */
private void getLine() {
  String line;                         // 1 line buffer as String
  int lineLength;	                      // character size of 1 line
  try {
    line = br.readLine();              // reads first line
  } catch (IOException e) {
    println("Read Error " + br.toString());
    return;
  }
  if (line == null) {                  // EOF is coming
     status.lineFeedFinal(emptyLine);
     EOF = true;
     return;
  }
  currentLineNumber++;
  status.lineFeed(emptyLine);
  emptyLine = true;
  indexOfLine = 0;                     // position of 1 line
  lineLength = line.length();          // set line length
  // if charBuff is overflow, then allocate charBuff
  if (lineLength >= CharBuffSize) {
     charBuff = new char[lineLength + 1];
     CharBuffSize = lineLength + 1;
  }
  line.getChars(0, lineLength, charBuff, 0); 	// for efficient using ChAry
  charBuff[lineLength] = '\0';         // set EOL mark
  return;			
}
	
/** get 1 Character */
private void getChar() {
  char nch = charBuff[indexOfLine];    // get Current Character
  while (nch == '\0') {
    getLine();                         // if end, then reads next line
    if (EOF) return;
    nch = charBuff[indexOfLine];
  }
  ch = charBuff[indexOfLine++];
}

/** 
 * get next 1 Character
 * @return char in current char (i.e., next char) 
 */
private char nextChar() {
  char nch = charBuff[indexOfLine];
  while (nch == '\0') {
    getLine();                         // if end, then reads next line
    if (EOF) return '\0';
       nch = charBuff[indexOfLine];
  }
  return charBuff[indexOfLine];
}

次に文字解析部のディスパッチャ部を持ってきます。1文字読み込んで、文字種類によって分岐する部分になります。

まずはシーケンシャルに分岐する方法で実装します。実装のコツとしては、分岐部分はなるべく1関数にすることにより、全体の見通しをよくするようにします。実際、1関数でほとんどを実装します。これにより、将来のリファクタリングでO(1)で実行する、例えばジャンプテーブルによる実装が行ないやすくなります。

/**
 * get Tokens via charBuff from source file
 */
private void getTokens() {
  while (true) {
    if (Character.isWhitespace(ch)) {               // WhiteSpace
	   ;                                            //   nothing process
    } else if (Character.isDigit(ch)) {             // Number
       setInitDefault();
    } else if (Character.isJavaIdentifierStart(ch) ||
               ch == '_') {	                       // Symbol
       symbolToken();                               // stackTop is keep
    } else {
       switch (ch) {
         case '/':                                  // commentable
           commentToken();                          // if not comment, not keeping
           break;                                   // if comment, comment is ignore
         case '"':                                  // String
           stringToken();
           break;
         case '(':                                  // OpenParen 
           openToken();
           break;
         case ')':                                  // CloseParen 
           closeToken();                            // stackTop is keep in toplevel
           break;
         case '{':                                  // OpenCurl 
           openCurlToken();
           break;
         case '}':                                  // CloseCurl 
           closeCurlToken();
           break;
         case ';':                                  // Close 
           semiColonToken();
           break;
         case '\'':                                 // SingleQuote 
           charToken();
           break;
         case ',':                                  // Comma
           symbol = null;                           // stakTop is keep
           emptyLine = false;                       // i.e., f() a, b {} is o.k.
           break;
         default:
           setInitDefault();                        // otherwise
           break;
       }
     }							
     if (!EOF) getChar(); else return;              // read next character
  } // end of while
}

ここまでは定型的なパターン、プログラムイディオムになっています。

次に各トークンの処理を見ていきましょう。例として、以下に '/' が来たときのコメント文処理のためのプログラムを示します。

/**
 * Comment statement --- skip comment end
 */
private void commentToken() {
  if (!EOF) getChar(); else return;
  if (ch == '/') {
    status.lineCommentIn();
    skipLineEnd();                      // skip line end
    status.lineCommentOut();
    return;
  } 
  if (ch == '*') {
     status.blockCommentIn();
     skipCommentEnd();                // skip comment end 
     status.blockCommentOut();
     return;
  }
  setInitDefault();
  return;
}

前半部で "//" から始まるラインコメント文の処理を行ないます。これは status.lineCommentIn() でラインコメントに入ったことを知らせ、次にskipLineEnd()で行末まで読み飛ばし、status.lineCommentOut()でラインコメントから外れたことを示します。後半部では status.blockCommentIn() でブロックコメントに入ったことを知らせ、次に skipCommentEnd() で "*/" が出現するまで読み飛ばすようにします。setInitDefault() は字句解析の初期状態に設定するための関数です。

同様に他のトークン、例えば、シンボルや文字列、数値なども同様にその状態に入ったことを知らせるメソッド関数を呼び出すようにします。知らせを受けたメソッド関数がどのように処理をするかについては後日に記述する予定です。

今日は文字解析の骨格部分をやってきました。明日はそこから使用する部品クラスについて紹介していきます。シンボルテーブルやファイルステータスなどを紹介する予定です。

See you again !!

このページトップへ

5日目 --- シンボルテーブルなどの部品クラスの作成

今日はシンボルテーブルなどの部品クラスを作っていきましょう。まずはシンボルテーブルを作ります。

シンボルテーブル

シンボルテーブルとは、この計測ツールで、対象とするプログラムのシンボルを管理するためのテーブルです。例えば、シンボルには Foo, int, return などの予約語や変数名、関数名などがあります。これらのシンボルがどういう種別(例えば、ステートメントを作る構成子なのか、(メトリクスにとっては)単なる名前なのか、などを区別し、また以前に出現したシンボルかどうかを判定するのに使います。

シンボルテーブルは、高速化のためにハッシュテーブルで実装します。今回の場合は HashMap で実装します。同期の責任はユーザが持つようにして高速化します。と言っても要素には即値型が格納できませんので、どちらにしろ遅いのは仕方がありません。

シンボルテーブルクラスを作成し、それにメトリクスツールが注目すべき予約語とその情報を格納するようにします。

package jp.co.okisoft.esc.metrics;

import java.util.HashMap;

/**
 * Symbol Table
 * @author gomi
 */
public class SymbolTable extends HashMap {
  public final static String CLASS_SYMBOL = "CLASS";
  public final static String SPECIAL_FORM = "SPECIAL FORM";
  private String[] reserveds = {
    "class", CLASS_SYMBOL, 
    "for", SPECIAL_FORM,
    "if", SPECIAL_FORM,
    "while", SPECIAL_FORM,
    "switch", SPECIAL_FORM,
  };
	
  /**
   * Constructor SymbolTable
   */
  SymbolTable(){
    super();
    initReserved();
  }
	
  /**
   * init reserved
   */
  private void initReserved () {
    for (int i = 0; i < reserveds.length; i += 2) {
      put(reserveds[i], reserveds[i + 1]);
    }
  }
}

SymbolTable extends HashMap で実装します。もちろん委譲モデルでの実装もありますが この場合は継承モデルの方が効率が良いでしょう。

次に属性情報としては、クラスを判別する識別子として CLASS_SYMBOL、とステートメントを 計測するときに特別扱いする予約語を SPCIAL_FORM として与えます。 reserveds に注目する予約語を名前、属性の順で格納します。 具体的には クラス識別子として、"class"、スペシャルフォームとして、"for", "if", "while", "switch" を入れています。 例えば、C# などの場合は foreach, using なども必要になってきます。

HashMap を継承していますので、セッタ、ゲッタはそれを使うようにします。 クラス名に意味論を明示化することと予約語設定のためにこのクラスを採用しています。

ファイルステータス

次にファイル単位のメトリクスデータを格納するためのクラス「ファイルステータス」を作成します。

package jp.co.okisoft.esc.metrics;

import java.util.ArrayList;

/**
 * class FileStatus
 */
public class FileStatus {
  private String fileName;
  private int totalLines;
  private int operationLines;
  private int statementLines;
  private int commentLines;
  private int emptyLines;
  private ArrayList functions;
  private ArrayList classes;
		
  /**
   *  Constructor of File Status
   */
  public FileStatus (String fileName, 
                     int totalLines, 
                     int operationLines,
                     int statementLines,
                     int commentLines,
                     int emptyLines,
                     ArrayList functions,
                     ArrayList classes) {
     this.fileName = fileName;
     this.operationLines = operationLines;
     this.totalLines = totalLines;
     this.statementLines = statementLines;
     this.commentLines = commentLines;
     this.emptyLines = emptyLines;
     this.functions = functions;
     this.classes = classes;
  } 
  /** getter */
  public String getFileName () { return fileName; }
  /** getter */
  public int getTotalLines () { return totalLines; }
  /** getter */
  public int getOperationLines () { return operationLines; }
  /** getter */
  public int getStatementLines () { return statementLines; }
  /** getter */
  public int getCommentLines () { return commentLines; }
  /** getter */
  public int getEmptyLines () { return emptyLines; }
  /** getter */
  public ArrayList getFunctions () { return functions; }
  /** getter */
  public ArrayList getClasses () { return classes; }
}

ここは、まさに生成時のセッタと、各ゲッタのみを用意したクラスです。Java ではこのような完全に受動的なオブジェクトでも構造体でなく、クラスで作成する必要があります。即ち、参照渡しになってしまい、コストが掛かります。

全体の流れ

字句解析「リーダ」でトークンを解析して取り出し、さらにプログラムの状態を構文解析をせずにまたは簡易な構文解析で解析を行います。このクラスは明日紹介する予定の「プログラムステータス」クラスになります。シンボルが出現したときは、シンボルテーブルによって、注目するシンボルかどうかを判定します。解析した結果をファイル単位で格納するクラスとしては「ファイルステータス」クラスを用意します。最後に結果を機能クラス「プリンタ」でファイルに出力します。

 

今日のステートメント数は40個も行っていません。小さなクラスとなっています。

明日はプログラムの状態を解析する「プログラムステータス」クラスの実装について記述していきます。

See you again !!

このページトップへ

6日目 --- プログラム解析・管理部の作成

今日はプログラム解析し、その情報を蓄積し管理する部分を作成していきます。先日のリーダを作るときには、例えば、 '{' が出現したときには

case '{':            // OpenCurl 
  openCurlToken();
  break;

のように openCurlToken メソッドを呼び出すところまでしかありませんでした。今日はその先をやっていくことにしましょう。

ここで、プログラムの解析の状態を管理するクラスとして、ProgramStatus を作成することにします。このインスタンスオブジェクトが、対象のプログラムの状態、例えば、クラス定義中なのか、メソッド定義中なのか、それともメッセージ通信を行うところなのかなどの状態を保持するようにします。この状態を保持することができるように、状態を変化させる要因、例えば、'{' のような注目する文字が出現したときには、状態変化を行う status のメソッドを呼び出すことにします。

例えば、openCurlToken メソッドでは、プログラムの解析状態を管理するオブジェクト "status" のメソッド openCurl を stackTop の値を引数として呼び出します。

/**
 *  OpenCurl '{' Token
 */		
private void openCurlToken() {
  status.openCurl(stackTop);    // add 1 to Curl Bracket level
  symbol = null;
  emptyLine = false;
  return;
}

フィールド変数 symbol は現在解析中のシンボルを保持するために用意したものです。'{' が出現しましたので、シンボル解析を抜け出したことを symbol = null によって、表現しています。またフィールド変数 emptyLine はコメントでないプログラムとして有効な文字が出現したかどうかのフラグとして使用しています。'{' のようにプログラムに有効な文字が出現しましたので、このフラグを false にしています。 

他の文字に対しても、同様な処理を行うことにします。

プログラムの各種状態を持つために、いくつかのフィールド変数を持ったクラス ProgramStatus を定義します。その宣言部は以下のようにしました。

package jp.co.okisoft.esc.metrics;

import java.util.ArrayList;

/**
 * Program Status Class
 * @author gomi
 */
public class ProgramStatus {
  // constant
  private final static int lineComment = 1;	// line comment
  private final static int blockComment = 2;  // block comment
  private final static int empty = 4;		// empty line or normal line
  private final static int normalMode = 0;
  private final static int classMode = 1;

  // public variable
  public int lineMode;			// current line mode 
  public static ArrayList files;	// file inf.

  // private variable
  private String fileName;		// file name
  private int curlLevel;		// Curl Bracket level
  private int operationLines;	// total operation line number
  private int statementLines;	// total statement line number
  private int totalLines;		// total source file line
  private int commentLines;		// comment line number
  private int emptyLines;		// emptyLine number
  private String functionSymbol;	// current function symbol
  private int startFunction;      // start position of Function defined 
  private int startLevelFunction; // start level of Function defined
  private ArrayList functions;    // function inf.
  private int statementMode;      // statement mode;
  private ArrayList classInf;
  private int currentClassCurlLevel = -1;

一方、解析した結果であるメソッドの情報を管理するための内部クラス MethodInf を以下のように定義します。

/** Method information */
class MethodInf {
  String methodName;
  int startLine;
  int startStatement;
  int curlLevel;
  String className;
  /** constructor */	
  MethodInf (String name, int lines, int statements, 
             int curl, String className) {
    this.methodName = name;
    this.startLine = lines;
    this.startStatement = statements;
    this.curlLevel = curl;
    this.className = className;
  }
}		

同様に、解析したクラスの情報を管理するためのクラス ClassInf を以下のように定義します。

/** Class information */
class ClassInf {
  String className;
  int startLine;
  int startStatement;
  int curlLevel;
  String fileName;
  /** constructor */	
  ClassInf (String name, int lines, int statements, 
            int curl, String file) {
    this.className = name;
    this.startLine = lines;
    this.startStatement = statements;
    this.curlLevel = curl;
    this.fileName = file;
  }
}		

これらの3個のクラスの役割分担は、解析中のプログラム全体の情報を管理する ProgramStatus 、クラス単位の情報を管理する ClassInf 、メソッド単位の情報を管理する MethodInf のようになります。ClassInf と MethodInf は受動的なクラスで、能動的に動作するのは ProgramStatus になります。

ここでも '{' が出現したときに動作する ProgramStatus のメソッド openCurl を見ていきます。

/**
 * open curl bracket
 */
public void openCurl (String symbol) {
  if (curlLevel == 1 + currentClassCurlLevel && symbol != null) {
     userDefinedFunction(symbol);
  }
  curlLevel++;
  return;
}

ここで、 フィールド変数 curlLevel は '{' の深さのレベルを保持しています。またフィールド変数 currentClassCurlLevel は現在のクラス定義を行ったときの '{' の深さのレベルを保持しています。メソッド userDefinedFunction はユーザ定義関数の登録を行うメソッドです。

ここでは、以下の規則で簡易的に、つまり本格的な構文解析をすることなしに、「ユーザ定義メソッドの開始」を発見しています。

  1. クラス定義したときの '{' の深さよりもちょうど1段だけ深い '{' の出現の前で(ここだけではわかりませんが '(', ')' が出現する前の) シンボル symbol がメソッド名であり、ここからメソッド定義が開始される

このような規則でメソッドを発見しています。

またクラス定義外のメソッド(または関数)定義は、クラスの '{' の深さレベルを -1 にすることにより、発見しています。

今日はプログラム解析クラス ProgramStatus の骨格を中心に進めてきました。プログラムステータスの残りの部分やリーダの残りの部分は後日、アップするようにします。明日は一次メトリクスデータの印字について、書いていきます。

See you again !!

 このページトップへ

7日目 --- メトリクスデータのファイル出力

今日は6日目までに収集したプログラムメトリクスデータをファイル出力するプログラムを作成します。

今回は簡単のために CSV 形式のファイルを出力するようにします。

中心となるメソッド printFiles を以下に紹介します。

すべてのメトリクスデータは、ファイル単位で管理していて、ProgramStatus の static 変数 files に格納されています。

ファイル単位のデータから全体のデータを導き出すメソッドは、ProgramStatus の totalStatus() です。また全体のデータのファイル出力を行うメソッドはファイル個別のメソッド printFileStatus() と共通化しています。第2引数に true を与えることにより、ファイル個別でなく全体のデータであることを示しています。

次に各クラスの情報や各メソッドの情報のファイル衆力部分は printFiles 自身にあります。少し長めになりますが、このメソッドを以下に示します。

	/**
	 * print files information to file
	 */	
	public void printFiles () {
		FileOutputStream file;	// file input stream for source program file
		try {
			file = new FileOutputStream(filesFile);
		} catch (FileNotFoundException e) {
			println("File " + filesFile + " is USED.");
			return;
		}
		OutputStreamWriter out = new OutputStreamWriter(file);	
		BufferedWriter bw = new BufferedWriter(out); // generats Buffer Writer
		ArrayList files = ProgramStatus.files;
		try {
			// total size
			FileStatus total = ProgramStatus.totalStatus();
			printFileStatus(total, true, bw);			 
			bw.newLine();
			bw.newLine();
			// each file
			for (int i = 0, size = files.size(); i < size; i++) {
				FileStatus fs 
					= (FileStatus) files.get(i);
				printFileStatus(fs, false, bw);
				
				// user defined class
				ArrayList classes = (ArrayList) fs.getClasses();
				for (int j = 0, fsize = classes.size(); j < fsize; j++) {
					ProgramStatus.ClassInf classInf 
						= (ProgramStatus.ClassInf) classes.get(j);
					bw.write(", Class, " + classInf.className);
					bw.write(", start line, " + classInf.startLine);
					bw.write(", statement number, " + classInf.startStatement);					
					bw.newLine();					
				}
			 
				// user defined methods
				ArrayList functions = (ArrayList) fs.getFunctions();
				for (int j = 0, fsize = functions.size(); j < fsize; j += 2) {
					if (j + 1 < fsize ) {
						if (functions.get(j + 1).getClass() == integerClass) {						
							bw.write(",, Function, " + functions.get(j) + 
								 ", statement number, " + functions.get(j + 1));
							bw.newLine();
							} else {
								j--;
							}
					}
				}
				bw.newLine();				
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		try {
			bw.close();
			out.close();
			file.close();
		} catch (IOException e) {
			println("File " + out + " is NOT CLOSE.");
			return;			
		}		
	}
  

クラスに関する情報は ClassInf オブジェクトとして格納しています。一方、メソッドの情報は名前とステートメント数の2項だけですので、 MethodInf オブジェクトではなく、アレイリストに直接代入しています。 

次に上記のメソッドで呼び出されるメソッド printFileStatus を紹介します。

	/**
	 * print file status
	 * @param fs File Status
	 * @param total Total Program or Each Function
	 * @param bw Buffered Writer to files.csv
	 * @exception IOException 
	 */
	private void printFileStatus(FileStatus fs, boolean total, 
								   BufferedWriter bw) throws IOException {			 
		if (total) {
			bw.write("Total Size, for all files");
		} else {
			bw.write("File Name, " + fs.getFileName());
		}			
		bw.newLine();
		bw.write(", Total Line Number, " + fs.getTotalLines());
		bw.newLine();
		bw.write(", Operation Line Number, " + fs.getOperationLines());
		bw.newLine();
		bw.write(", Statement Number, " + fs.getStatementLines());
		bw.newLine();
		bw.write(", Comment Line Number, " + fs.getCommentLines());
		bw.write(checkMetrics(1, fs.getCommentLines(), fs.getTotalLines())); 
		bw.newLine();
		if (total) {
			double a1 = ((Double) fs.getFunctions().get(0)).doubleValue();
			double a2 = ((Double) fs.getFunctions().get(2)).doubleValue();
			double a3 = ((Double) fs.getClasses().get(0)).doubleValue();
			double ret = Evaluation.programRate(a1, a2, a3);

			bw.write(", Average Statements a Class, " + a3);
			bw.write(", Class Number, " + fs.getClasses().get(1));
			bw.newLine();
			bw.write(", Average Statements a Method, " + a1);
			bw.write(", Method Number, " + fs.getFunctions().get(1));
			bw.write(", Average Methods a Class, " + a2);
			bw.newLine();
			bw.write(", Program Rate, " + ret);			
			bw.write(", Program Quantity, " + ret * fs.getStatementLines());			
			bw.newLine();
		}			
		
	}

全体のメトリクスデータのときに実行される a1 〜 a3 を使って表示する部分が、メトリクスデータの統計値になります。

a1 は平均メソッドステートメント数、a2 は平均クラスメソッド数、a3 は平均クラスステートメント数になります。

そして Evaluation.programRate(a1, a2, a3) はこれらの3個の数値を使って評価する「プログラム率」を返します。これはプログラムの「良さ」になります。平均値は1となるように調整しています。

このプログラム率がこのメトリクスツールの眼目になります。まだここでは不明の Evaluation.programRate で計算した結果を持ってくるところから、この評価関数で悪い点を取ったプログラマから、「いちゃもん」と呼ばれているのもここからになります。

後は、片手間に各種の警告も出力するようにしています。例えば、コメント行が一定に比率以下であるときに警告を出します。そこのプログラムを以下に示します。

	/** threshold for metrics */
	private static int commentThreshold = 20;
	 
	/**
	 * checkMetrics
	 * @param number Metrics Number
	 * @param m1 Metrics data 1
	 * @param m2 Metrics data 2
	 */
	private String checkMetrics(int number, int m1, int m2) {
		switch (number) {
			case 1:
				if (m2 == 0) return "";
				if (m1 * 100 / m2 < commentThreshold) 
					return ", too few comment";
				else
					return "";

つまり、閾値と分母、分子を与えて、越えていれば、メッセージを文字列として返すメソッドになっています。

他の警告として、1行も文字数が多すぎると、警告を出すようにしています。

今日はメトリクスデータのファイル出力について、書いてきました。 Evaluation.programRate のメソッドを除けば、一通りのメトリクスツールの作成について述べてきました。明日は残りの部分やファイルの出力結果などを報告する予定にしています。

See you again !!

 このページトップへ

8日目 --- まとめと結果のサンプル

昨日まででソースプログラムの入力、メトリクス計測から結果のファイル出力までを書いてきました。今日は今までのまとめを書いていきます。

まずは自分自身を自分自身で計測してみます。

Total Size, for all files
	Total Line Number			1562
	Operation Line Number		1037
	Statement Number		 	 758
	Comment Line Number	 	 522

全体の総行数は 1562 行で、命令行数は 1037行、ステートメント数は 758、コメント行は 522 行であることがわかります。但し、ここでコメント行とは、コメントが少しでもある行のことで、コメントだけの行のことを指していません。

500行程度で作成する予定でしたが、命令行数で約 1000行、ステートメントで 約 750 になっています。 

次にクラス数やメソッド数、さらにそれぞれの平均ステートメント数などを以下に示します。

	Average Statements a Class	  61.2
	Class Number			  13
	Average Statements a Method	   7.1
	Method Number			  87
	Average Methods a Class	   6.7

1クラスの平均ステートメント数は 61.2、1メソッドの平均ステートメント数は 7.1、1クラス当たりの平均メソッド数は 6.7 個であることが分かります。なお、平均メソッド数/クラス×平均ステートメント数/メソッド = 6.7 × 7.1 が1クラスの平均ステートメント数に一致しないのはフィールド変数の初期設定や static 文などのためです。

次にプログラム率とプログラム生産量を見てみます。

Program Rate	  	  0.98
Program Quantity		743.1

プログラム率が 0.98 で、プログラム生産量は 743.1 であることが分かります。プログラム率は 1.00 を中心とした指標ですので、0.98 はほぼ平均的なプログラムであることが言えます。758ステートメントを作成しましたが、このツールの評価では 743 ステートメントとして評価しています。

次にファイル単位の計測結果を見てみます。

File Name	jp/co/okisoft/esc/metrics/Evaluation.java
	Total Line Number		95
	Operation Line Number	69
	Statement Number		49
	Comment Line Number	39
Class	Evaluation	start line	6		statement number	48
	Function	setStandardWeight		statement number	 9
	Function	setSimpleWeight		statement number	 5
	Function	programRate			statement number	 9
	Function	programRate			statement number	 6
	Function	programQuantity		statement number	 4
	Function	programQuantity		statement number	 3
	Function	main				statement number	 3

ファイル Evaluation.java の計測結果です。ファイル単位の総行数などの情報が前述の全体の情報と同じように出力されています。

次にこのファイルで定義されているクラスとして、Evaluation があり、6行目から開始され、48 ステートメントであることがわかります。

最後にこの Evaluation クラスのメソッド関数の名前とステートメント数が示されています。同じメソッド名があるものはオーバーロードされているメソッドになります。

このメトリクスツールのプログラムは、自分自身に対して警告は出ませんでした。

もし、コメント率や1行の文字数が長すぎるなどの一般的には複雑すぎるプログラム断片を発見したときには、各種警告を出せるようになっています。

これでメトリクスツール「いちゃもん」の作り方は終了になります。最後まで読んでくださってありがとうございました。

大きく拡張したときには続編も書いていこうと思います。

See you again !!

 このページトップへ


Copyright (c) 2004, 2005 OKI Software Co., Ltd.