pythonとgraphvizでビジュアルmakedepのようなものを作ってみた

仕事で、C/C++ソースコードリファクタリング(ほとんどリバースだけど)をする必要が出てきました。コードを見ると、無節操にヘッダーファイルに別のヘッダーファイルをインクルードしているため、コードの階層分けすら困難を極めそうな状況です。

で、まずはヘッダーファイルの依存関係をグラフ化して見えるようにしたいなと思い、pythonでちょこちょことツールを
作ってみました。ソースコードから '#include "xxxxx"'を reパターンマッチで拾い出して、ファイルのインクルード関係をディクショナリに記録し、最後にgraphvizでグラフ表示するという簡単なツールです。

同じような苦労をしている方の何かの参考になれば幸いです。

#!/usr/local/bin/python
# coding: utf8
################################################################################
# incdep.py
# Description:  C言語のインクルードファイルの依存グラフを生成
#
# Author:       shozo fukuda
# Date:         Fri Jan 06 09:52:05 2012
# Last revised: $Date$
# Application:  Python 2.7.2
################################################################################

#<IMPORT>
import os,sys,string
import re
import pydot
import getopt

#<SUBROUTINE>###################################################################
# Function:     使用例の表示
# Description:  
# Dependencies: 
################################################################################
def usage():
  program = os.path.basename(sys.argv[0])
  print u"""
C言語のインクルードファイルの依存関係グラフ作成

  Usage: %s [-I<dir>] <ソースファイル名>
  
    -I<dir> ディレクトリ<dir>の中も検索します

""" %(program)

#<SUBROUTINE>###################################################################
# Function:     ファイル検索
# Description:  名前が<name>のファイルをカレント・ディレクトリまたは検索ディレク
#               トリ・リスト<dirs>で検索する。ファイルが見つかった場合は、ファイ
#               ルのそのパス名を返す。ファイルが見つからない場合はNoneを返す。
# Dependencies: 
################################################################################
def findfile(name, dirs, curdir='.'):
  """
  ファイル検索

  名前が<name>のファイルをカレント・ディレクトリ<curdir>、検索ディレクトリ・リス
  ト<dirs>の順に検索する。ファイルが見つかった場合は、その    パス名を返す。
  ファイルが見つからない場合はNoneを返す。
  """
  path = os.path.join(curdir, name)
  if os.path.isfile(path):
    return path
  
  for dir in dirs:
    path = os.path.join(dir, name)
    if os.path.isfile(path):
      return path

  return None

#<CLASS>########################################################################
# Class:        インクルードファイルの依存関係辞書
# Description:  
# Dependencies: 
################################################################################
class Mkdep(dict):
  re_include = re.compile(r'#\s*include\s+\"(.+)\"')
  tr_sep     = string.maketrans('\\', '/')


  def __init__(self, name):
    name = Mkdep.abspath(name)
    self.list = [name]   # 対象ファイル・リスト
    self.dirs = []       # 検索ディレクトリ・リスト
    self.home = os.getcwd()


  @staticmethod
  def abspath(name):
    """ ファイル<name>の絶対パスを返す。パスの区切り文字は'/'に置き換える。"""
    return Mkdep.normpath(os.path.abspath(name))


  @staticmethod
  def relpath(name, curdir):
    """
    ファイル<name>のディレクトリ<curdir>からの相対パスを返す。パスの区切り文字
    は'/'に置き換える。
    """
    return Mkdep.normpath(os.path.relpath(name, curdir))


  @staticmethod
  def normpath(name):
    """ ファイル<name>の文字列を正規化する。 """
    return os.path.normpath(os.path.normcase(name)).translate(Mkdep.tr_sep)


  def add_dir(self, dir):
    """ ファイルを検索するディレクトリを登録する。 """
    if dir not in self.dirs:
      self.dirs.append(dir)


  def gather(self):
    """ #include宣言からファイルの依存関係を調べて辞書をつくる。 """
    for father in self.list:
      cwd = os.path.dirname(father)

      self[father] = []
      f = open(father)
      for text in f:
        match = Mkdep.re_include.match(text)
        if match:
          child = findfile(match.group(1), self.dirs, cwd)
          if child:
            child = Mkdep.normpath(child)
            self[father].append(child)
            if child not in self.list:
              self.list.append(child)
          else:
            child = Mkdep.normpath(match.group(1))
            self[father].append(child)
      f.close()


  def write_gif(self, name):
    """ 依存関係グラフをGIFフォーマットで作成・保存する。 """
    graph = pydot.Dot()
    graph.set_graph_defaults(rankdir='LR')
    graph.set_node_defaults(shape='box', style='rounded', fontname="Arial")
    for (father, children) in self.iteritems():
      father = Mkdep.relpath(father, self.home)
      for child in children:
        child = Mkdep.relpath(child, self.home)
        edge = pydot.Edge(father, child)
        graph.add_edge(edge)
    graph.write_gif(name)


  def dump(self):
    """ 依存関係辞書を印字する。 """
    for (father, children) in self.iteritems():
      for child in children:
        print "%s,%s" %(father, child)


#<MAIN>#########################################################################
# Function:     メインルーチン
# Description:  簡易コマンドラインI/F
# Dependencies: 
################################################################################
if __name__ == '__main__':
  try:
    f = open('incdep.cfg')
    cfgs = f.read().split()
    f.close()
  except IOError:
    cfgs = []

  try:
    opts, args = getopt.getopt(cfgs + sys.argv[1:], "I:")
  except getopt.error, v:
    print "Error:", v
    usage()
    exit(1)

  if not args:
    print "Error: expect file name"
    usage()
    exit(1)

  src = args[0]
  dst = os.path.splitext(src)[0] + '.gif'

  mkdep = Mkdep(src)
  for opt, val in opts:
    if opt == '-I': mkdep.add_dir(val)

  mkdep.gather()

  mkdep.write_gif(dst)

# incdep.py