DoxygenのXML出力

 ひさびさに、仕事で(C++ではない)C言語を触っている。コーディングのお約束として、基本はグローバル変数、関数パラメータは使わない、構造体も使っちゃダメ、ということになってる。特殊用途なので仕方ないらしい。

 ただ、こうなると、動作の解析が一筋縄ではいかないので、グローバル変数の一覧、参照範囲等をまとめた資料がほしくなる。という訳で、DoxygenXML出力をちょこっと整形出力してみた。



出力はこんな感じになる。
Excelに読込むと、関数の上下関係の概略と、どの関数がどの変数を参照しているのか、多少なりとも分かりやすくなる。

ファイル名,L1,L2,L3,L4,L5,L6,L7,L8,L9,L10,参照変数名,説明
foo.c,hoge_sch,,,,,,,,,,,1000 XXXスケジューラ 
foo.c,,hoge_cnt,,,,,,,,,,1100 XXX管理 
foo.c,,,hoge_ini,,,,,,,,,1200 XXX入力管理
foo.c,,,,hoge_xxx,,,,,,,,
,,,,,,,,,,,v_yyy,○○○
,,,,,,,,,,,v_zzz,○○○
                 ・
                 ・
                 ・
foo.c,,,,hoge_yyy,,,,,,,,1300 XXXパラメータ設定 
,,,,,,,,,,,v_yyy,○○○
,,,,,,,,,,,v_zzz,○○○
                 ・
                 ・
                 ・


処理を行ったPythonスクリプトは、次のとおり。

# -*- coding: utf-8 -*-

import argparse
import os
import sys
import xml.etree.ElementTree as ET

Doxygen = None

def SetupDoxygen(directory, entry):
  global Doxygen
  Doxygen = DoxygenXml(directory, entry)


class Member(object):
  """DoxygenのXML出力のmember要素に相当するクラス
  """

  __members = {}

  @classmethod
  def get(klass, id, c_ref=None):
    if not klass.__members.has_key(id):
      klass.__members[id] = None #循環参照を回避するため、仮エントリ登録
      aMember = Member(c_ref, id)
      klass.__members[aMember.id] = aMember
    return klass.__members[id]

  def __init__(self, c_ref, id):
    self.c_ref = c_ref
    self.id = id
    self.filename = Doxygen.getCompound(c_ref).find('name').text
    element = self.getMember()
    #-----
    self.kind = element.attrib['kind']
    self.name = element.find('name').text
    self.para = Doxygen.text(element.find('./detaileddescription/para'))

    if self.kind == 'function':
      self.funcs = {}
      self.vars = {}
      for ref in element.findall("./references"):
        c_ref = ref.attrib['compoundref']
        m_ref = ref.attrib['refid']
        ref = Member.get(m_ref, c_ref)
        if ref.kind == 'variable':
          self.vars[ref.id] = ref
        if ref.kind == 'function':
          self.funcs[ref.id] = ref

  def getMember(self):
    return Doxygen.getMember(self.c_ref, self.id)


class DoxygenXml(object):
  """DoxygenのXML出力からデータ取得を行うクラス
  """
  def __init__(self, directory, entry):
    self.directory = directory
    self.entry = entry
    self.index_xml = os.path.join(self.directory, 'index.xml')
    self.doxygenindex = ET.parse(self.index_xml).getroot()

  def getEntry(self):
    for c in self.doxygenindex.findall("./compound[@kind='file']"):
      for f in c.findall("./member[@kind='function']"):
        name = f.find('name')
        if name.text == self.entry:
          return Member.get(f.attrib['refid'], c.attrib['refid'])
    return None

  def getCompound(self, c_refid):
    return self.doxygenindex.find("./compound[@refid='%s']" % c_refid)

  def getMember(self, c_refid, f_refid):
    #print os.path.join(self.directory, c_refid+os.extsep+'xml')
    tree = ET.parse(os.path.join(self.directory, c_refid+os.extsep+'xml'))
    root = tree.getroot()
    return root.find("./compounddef/sectiondef/memberdef[@id='%s']" % f_refid)

  def getAllFiles(self):
    return self.doxygenindex.findall("./compound[@kind='file']")

  def getMembers(self, kind=None):
    refs = []
    for compound in self.getAllFiles():
      name = compound.find('name')
      c_refid = compound.attrib['refid']
      if kind is None:
        xpath = "./member"
      else:
        xpath = "./member[@kind='%s']" % kind
      for member in compound.findall(xpath):
        ref = Member.get(member.attrib['refid'], c_refid)
        refs.append(ref)
    return refs

  @staticmethod
  def text(element):
    if element is None:
      return ""
    else:
      return element.text


class Process(object):
  """DocygenのXMLをCSVに変換する処理
  """
  def __init__(self):
    #self.encode = 'sjis'
    #sjisだと、'〜'、'‖'、'−'、'¢'、'£'、'¬'などの一部の記号類が文字化けする
    self.encode = 'cp932'

  def output(self, msg):
    print msg.encode(self.encode)

  def do(self):
    entry = Doxygen.getEntry()
    if entry is None:
      self.processWithoutEntry()
    else:
      self.processWithEntry(entry)

  def processWithEntry(self, entry, level=0):
    max_level = 10
    if level==0:
      self.output(u"ファイル名,L1,L2,L3,L4,L5,L6,L7,L8,L9,L10,参照変数名,説明")
    if entry is None:
      return
    try:
      self.output((u"%s,"+u","*level+u"%s"+u","*(max_level-level)+u",%s")
                 % (entry.filename, entry.name, entry.para))
    except Exception, e:
      self.output((u"%s,"+u","*level+u"%s"+u","*(max_level-level)+u",%s")
                 % (entry.filename, entry.name, u'<変換エラー:おそらくエンコードが違います>'))
      raise e
    #参照変数
    for v in entry.vars:
      m = Member.get(v)
      self.output((u","*(1+max_level)+u"%s,%s")
                  % (m.name, m.para))
    #参照関数
    for f in entry.funcs:
      self.processWithEntry(Member.get(f), level+1)

  def processWithoutEntry(self):
      self.output(u"ファイル名, 関数名, 参照変数名")
      for f in Doxygen.getMembers('function'):
        print "%s, %s, %s" % (f.filename, f.name, '')
        for v in f.vars:
          e = Member.get(v)
          print "%s, %s, %s" % ('', '', e.name)

def main():
  parser = argparse.ArgumentParser(description="""Doxygen-XML出力解析""")
  parser.add_argument('-d', '--directory',default="."+os.sep+'generated'+os.sep+'xml')
  parser.add_argument('-e', '--entry',default="hoge_sch")
  args = parser.parse_args()
  SetupDoxygen(args.directory, args.entry)
  Process().do()


if __name__ == '__main__':
  main()


簡単なスクリプトでこんなことができるのは、Doxygenのおかげ。Doxygenエラい。各関数のソースコードも特定できるので、ちょっと手を加えると、フローチャートの自動生成もできるかもしれない。