EXCEL VBAのプログラム説明書をプログラムから自動作成

## VBAのプログラム説明書の必要性
VBAで作られたシステムを保守するとき、システム設計書がない、あっても最新ではない。などで、現行のＶＢＡプログラムの解析、リファクタリングに相当の工数を要した経験から、ＶＢＡプログラムソースからプログラム説明書を作るＶＢＡプログラムを作成中です。
プログラムの構造はそのままで、適当なコメントを挿入していく必要があります。
あるシステムでは合計有効行数5000行のプログラムの分析しながら約1000行（２割）のコメントを入れました。
@Mikoshiba_Kyu さんの[記事](https://qiita.com/Mikoshiba_Kyu/items/46b7243eb576848b3e55)を大いに参考にして作成しました。優れている点も多いのでぜひご覧ください。
　なお、分析するエクセルブックを開いて操作しますので、コピーを対象にするなど自己責任でお使いください。
### 出力例
このVBAソースを分析出力した一部です

![VBAプログラム説明書.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3655346/c1895161-7c74-0991-73f7-ba70c7b8227b.png)


 
 ### 定数定義
 ```vb
 Option Explicit
'--------------------------------------------------
'>> プロシージャ・ファンクションの概要説明書を作成する
'> [使用上の注意]対象のブックをオープンするので念の為コピーを対象にすること
'> 他のエクセルブックは閉じておいてください。
'> 自己責任で使用してください。
'> いろいろ漏れがあると思いますので自由に改変してください。
'> それぞれの記法に合わせるには　fg_lineType　あたりを変更してください
'> 下記は省略しています。ご希望があれば追加で掲載します
'> 処理速度向上のための非表示処理など
'> 利用している外部DBのテーブル名の表示
'--------------------------------------------------

'> 定数変数を定義
Const TITLE_ROW = 2 'タイトル行番号

Dim code_count As Long  '有効行数
Dim in_line As String   'ソース一行
Dim gaiyo_line As String 'プロシージャ概要
Dim comment_line As String  'プロシージャコメント
Dim pro_line As String  '有効ソース行

Dim isModuleBegin As Boolean    'モジュール開始
Dim module_gaiyo_line As String  'モジュール概要
Dim module_comment_line As String 'モジュールコメント
Dim code_count_mod As Long  '有効行数モジュール計
Dim inBookName As String '分析するエクセルブック
Dim outWorkbook As Workbook '出力するエクセルブック
Dim outWorksheet As Object '出力するエクセルシート
Dim procedureNames As Collection 'プロシージャ一覧
Dim I As Long
Dim I_mod As Long
Dim xlModule As Object
Dim myCodeModule As Object
Dim moduleName As String
Dim moduleType As String

Dim procedureName As String
Dim startLine As Long  'プロシージャ開始行
Dim endLine As Long 'プロシージャ終了行
'> 列の位置を定義
Enum itemCOL
    左端列 = 1
    モジュール名
    モジュール型
    プロシージャ名
    プロシージャ型
    概要
    処理内容
    呼び出
    行位置
    行数
End Enum
 ```
 ### メイン部
```vb
'--------------------------------------------------
'プロシージャ・ファンクションの概要説明書を作成する（メインフロー）
'--------------------------------------------------
Public Sub makeVBA_Document()
    '1.分析するエクセルブックを選択して開く
    inBookName = openVBAFILE
    '2.選択しなければ終了
    If inBookName = "" Then Exit Sub
    '3.出力するワークブックを新規追加
    Set outWorkbook = Workbooks.Add
    '4.出力するワークシートを設定
    Set outWorksheet = outWorkbook.ActiveSheet
    '5.全プロシージャ名を取得してコレクションに設定する
    Call setProcedureNames
    '6.プロシージャ毎の初期処理
    Call initProcedure
    
    With outWorksheet
    
    I = TITLE_ROW
    
    '7.対象ブックの各モジュールを処理する
    For Each xlModule In Workbooks(inBookName).VBProject.VBComponents
        '7.1.モジュールのコードを取得
        Set myCodeModule = xlModule.codemodule
        
        '7.2.モジュール初期処理
        I = I + 1
        moduleName = xlModule.Name
        moduleType = xlModule.Type
        
        .Cells(I, itemCOL.モジュール名) = moduleName
        .Cells(I, itemCOL.モジュール型) = fg_moduleType(moduleType)
        
        Call initModule
        procedureName = ""
        '7.3.モジュールコードの各行を処理
        Dim L As Long
        For L = 1 To myCodeModule.CountOfLines
        
            If procedureName <> myCodeModule.ProcOfLine(L, 0) Then
            '7.3.1.プロシージャが変わったときの処理
                Call procedureChange(L)
            End If
            '7.3.2.現在行を取得する
            in_line = myCodeModule.Lines(L, 1)
            
            Select Case L
                Case Is < startLine
                    '7.3.3.プロシージャ宣言行の前の処理
                    Call editBeforeProcedure
                Case Is = startLine
                    '7.3.4..プロシージャ宣言行の処理
                    Call editTeigiLine
                Case Is < endLine
                     '.プロシージャ内容行の処理
                    Call editProcedureNaiyo
                Case Is = endLine
                    '7.3.5.プロシージャ最終行の処理
                    Call writeProcedureLine
            End Select
        Next L
        '7.4.モジュール開始フラグ＝Trueのときモジュール開始処理
        If isModuleBegin Then
            Call moduleBegin
            isModuleBegin = False
        End If
        
    Next xlModule
    
    '8.分析したエクセルブックを閉じる
    If inBookName <> ThisWorkbook.Name Then Workbooks(inBookName).Close False
    '9.ワークシートの書式を設定する
    Call layoutSheet(I)
        
    End With
   
    MsgBox "モジュール分析出力しました。エクセルブックを保存して下さい。"
    Windows(ThisWorkbook.Name).Activate
    Windows(outWorkbook.Name).Activate

End Sub
 
 ```
 ### サブルーチン
```vb
'--------------------------------------------------
'プロシージャが変わったときの処理
'--------------------------------------------------
Private Sub procedureChange(ByVal L As Long)
    '1.プロシージャ初期処理
    Call initProcedure
    '2.プロシージャ名を取得
    procedureName = myCodeModule.ProcOfLine(L, 0)
    
    On Error Resume Next
    '3.プロシージャ開始行番号を取得
    startLine = myCodeModule.ProcBodyLine(procedureName, 0)
    If Err.Number <> 0 Then
        MsgBox Err.Description
        Err.Clear
    End If
    On Error GoTo 0
    '4.プロシージャ終了行番号を計算
    endLine = myCodeModule.ProcStartLine(procedureName, 0) + myCodeModule.ProcCountLines(procedureName, 0) - 1
End Sub
'--------------------------------------------------
'各行の種類を返す
' 引数　入力行
'戻り値　入力行の種類（LONG)
'--------------------------------------------------
Private Function fg_lineType(ByVal p_line As String) As Long
    Dim w_line As String    
    '1.入力行の前後の空白を除く
    w_line = Trim(p_line)
    
    '2.空白行、区切り表示行なら0を返す
    If (w_line = "") Or (Left(w_line, 4) = "'---") Then
        fg_lineType = 0
        Exit Function
    End If
    '3.モジュール概要なら3を返す
    If Left(w_line, 3) = "'>>" Then
        fg_lineType = 3
        Exit Function
    End If
    '4.モジュール説明なら4を返す
    If Left(w_line, 2) = "'>" Then
        fg_lineType = 4
        Exit Function
    End If    
    '5.コメント行なら2を返す
    If Left(w_line, 1) = "'" Then
        fg_lineType = 2
        Exit Function
    End If
    '6.上記以外は1を返す（一般行）
    fg_lineType = 1
End Function
'--------------------------------------------------
'プロシージャ宣言の前の内容から編集する
'--------------------------------------------------
Private Sub editBeforeProcedure()
    Select Case fg_lineType(in_line)
'1.入力行を判定して該当の処理を実行
        Case 3
        '1.1.モジュール概要行編集
            module_gaiyo_line = module_gaiyo_line & Trim(Mid$(in_line, 4)) & vbLf
        Case 4
        '1.2.モジュール説明行編集
            module_comment_line = module_comment_line & Trim(Mid$(in_line, 3)) & vbLf
        Case 2
        '1.3.プロシージャ概要行編集
           gaiyo_line = gaiyo_line & Mid(Trim(in_line), 2) & vbLf
    End Select
End Sub
'--------------------------------------------------
'プロシージャ宣言行のとき編集出力する
'--------------------------------------------------
Private Sub editTeigiLine()
    With outWorksheet    
        If isModuleBegin Then
        '1.モジュールの出力が未だなら概要・処理内容を出力する
            I_mod = I
            .Cells(I, itemCOL.概要) = module_gaiyo_line
            .Cells(I, itemCOL.処理内容) = module_comment_line            
            isModuleBegin = False
        End If
        '2.一行追加して基本項目を出力
        I = I + 1
        .Cells(I, itemCOL.モジュール名) = moduleName
        .Cells(I, itemCOL.モジュール型) = fg_moduleType(moduleType)
        .Cells(I, itemCOL.プロシージャ名) = procedureName
        .Cells(I, itemCOL.プロシージャ型) = fg_procedureType(in_line)
    End With
End Sub
'--------------------------------------------------
'プロシージャ内容行のとき編集する
'--------------------------------------------------
Private Sub editProcedureNaiyo()
    Select Case fg_lineType(in_line)
        Case 1
        '1.一般のコード行なら呼び出しているプロシージャ名を取得する
            code_count = code_count + 1
            Call setUseProcedure(procedureName)
        Case 2
        '2.コメント行ならコメント編集行に追加し改行する
            comment_line = comment_line & Mid(Trim(in_line), 2) & vbLf
    End Select
End Sub
'--------------------------------------------------
'プロシージャ行を出力する
'--------------------------------------------------
Private Sub writeProcedureLine()
    '1.プロシージャの概要、処理内容、呼び出、行位置、行数を出力する
    With outWorksheet
        .Cells(I, itemCOL.概要) = gaiyo_line
        .Cells(I, itemCOL.処理内容) = comment_line
        .Cells(I, itemCOL.呼び出) = pro_line
        .Cells(I, itemCOL.行位置) = startLine
        .Cells(I, itemCOL.行数) = code_count
    End With
End Sub
'--------------------------------------------------
'呼び出しているプロシージャ名を編集する
'--------------------------------------------------
Private Sub setUseProcedure(ByVal inName As String)
    Dim proName As Variant
    For Each proName In procedureNames
    '1.プロシージャ名一覧コレクションを検索し
        If (proName <> inName) And InStr(in_line, proName) > 0 Then
        '1.1.ヒットしたら呼び出し編集行に追加し改行する
            If InStr(pro_line, proName) = 0 Then
               pro_line = pro_line & proName & vbLf
            End If        
        End If
    Next
End Sub
'--------------------------------------------------
'プロシージャ毎の初期化
'--------------------------------------------------
Private Sub initProcedure()
    '1.作業行やカウンターをクリアする
    comment_line = ""
    gaiyo_line = ""
    pro_line = ""
    code_count = 0
End Sub
'--------------------------------------------------
'全プロシージャ名をコレクションに設定する
'--------------------------------------------------
Private Sub setProcedureNames()
    Set procedureNames = New Collection
    Dim xlModule As Object
    Dim myCodeModule As Object    
    For Each xlModule In Workbooks(inBookName).VBProject.VBComponents
        Set myCodeModule = xlModule.codemodule
        Dim procName As String: procName = ""
        Dim L As Long        
        For L = 1 To myCodeModule.CountOfLines
            If myCodeModule.ProcOfLine(L, 0) <> procName Then            
                procName = myCodeModule.ProcOfLine(L, 0)
                procedureNames.Add procName            
            End If       
        Next L        
    Next xlModule
End Sub
'--------------------------------------------------
'モジュール毎の初期化
'--------------------------------------------------
Private Sub initModule()
    '1.isModuleBegin = True に設定
    isModuleBegin = True
    '2.作業行やカウンターをクリアする
    module_gaiyo_line = ""
    module_comment_line = ""
    code_count_mod = 0
    startLine = 9999999
    endLine = 9999999
End Sub
'--------------------------------------------------
'モジュール型を返す
'引数：モジュール型(Long)
'戻り値:モジュール型名(String)
'--------------------------------------------------
Private Function fg_moduleType(ByVal moduleTyoe As Long) As String
    Select Case moduleType
        Case 1
            fg_moduleType = "標準モジュール"
        Case 2
            fg_moduleType = "クラスモジュール"
        Case 3
            fg_moduleType = "ユーザーフォーム"
        Case 100
            fg_moduleType = "Excelオブジェクト"
        Case Else
            fg_moduleType = "その他"
    End Select
End Function
'--------------------------------------------------
'プロシージャ型を返す
'引数：宣言行(string)
'戻り値:プロシージャ型名(String)
'--------------------------------------------------
Private Function fg_procedureType(ByVal p_line As String) As String
    Select Case True
        Case Trim(p_line) Like "Private *"
            fg_procedureType = "Private"
        Case Trim(p_line) Like "Friend *"
            fg_procedureType = "Friend"
        Case Trim(p_line) Like "Static *"
            fg_procedureType = "Static"
        Case Else
            fg_procedureType = "Public"    
    End Select
End Function
'--------------------------------------------------
'モジュールの概要等が未出力のとき出力する
'--------------------------------------------------
Private Sub moduleBegin()
    With outWorksheet
        I_mod = I
        .Cells(I, itemCOL.概要) = module_gaiyo_line
        .Cells(I, itemCOL.処理内容) = module_comment_line
    End With
End Sub
'--------------------------------------------------
'ワークシートの書式設定
'--------------------------------------------------
Private Sub layoutSheet(ByVal lastRow As Long)
    With outWorksheet
        '1.項目名を設定
        .Cells(TITLE_ROW, itemCOL.モジュール名).Value = "モジュール名"
        .Cells(TITLE_ROW, itemCOL.モジュール型).Value = "モジュール型"
        .Cells(TITLE_ROW, itemCOL.プロシージャ名).Value = "プロシージャ名"
        .Cells(TITLE_ROW, itemCOL.プロシージャ名).Value = "プロシージャ名"
        .Cells(TITLE_ROW, itemCOL.プロシージャ型).Value = "プロシージャ型"
        .Cells(TITLE_ROW, itemCOL.概要).Value = "概要"
        .Cells(TITLE_ROW, itemCOL.処理内容).Value = "処理内容"
        .Cells(TITLE_ROW, itemCOL.呼び出).Value = "呼び出"
        .Cells(TITLE_ROW, itemCOL.行位置).Value = "先頭行番号"
        .Cells(TITLE_ROW, itemCOL.行数).Value = "有効行数"
        '2.項目列幅を設定
        .Cells(TITLE_ROW, itemCOL.モジュール名).ColumnWidth = 20
        .Cells(TITLE_ROW, itemCOL.モジュール型).ColumnWidth = 10
        .Cells(TITLE_ROW, itemCOL.プロシージャ型).ColumnWidth = 10
        .Cells(TITLE_ROW, itemCOL.概要).ColumnWidth = 30
        .Cells(TITLE_ROW, itemCOL.処理内容).ColumnWidth = 65
        .Cells(TITLE_ROW, itemCOL.呼び出).ColumnWidth = 20
        .Cells(TITLE_ROW, itemCOL.行位置).ColumnWidth = 5
        .Cells(TITLE_ROW, itemCOL.行数).ColumnWidth = 5
        '3.折り返し設定
        .Range(Cells(TITLE_ROW, 1), Cells(TITLE_ROW, itemCOL.行数)).WrapText = True
        .Range(Cells(TITLE_ROW + 1, itemCOL.モジュール名), Cells(lastRow, itemCOL.呼び出)).WrapText = True
        '4.罫線を引く
        .Range(Cells(TITLE_ROW, itemCOL.モジュール名), Cells(lastRow, itemCOL.行数)).Borders.LineStyle = xlContinuous  
    End With
End Sub
```
### 対象ファイルを選択しオープン
```vb
Public Function openVBAFILE() As String

    Dim targetFileName As String
    
    targetFileName = Application.GetOpenFilename("マクロ有効　EXCELブック, *.xlsm")
    If targetFileName = "False" Then Exit Function
    
    Application.EnableEvents = False
    Workbooks.Open targetFileName, ReadOnly:=True
    Application.EnableEvents = True
    
    If Not (ActiveWindow Is Nothing) Then
        ActiveWindow.Visible = False
    End If
    openVBAFILE = Mid(targetFileName, InStrRev(targetFileName, "\") + 1)
 
End Function