# プログラム設計書 30-diagnostics

## 1. 概要

### このプログラムの目的

このプログラムは、Luaソースコードの問題（エラー、警告、情報）を検出する「診断」機能を統括するためのものです。未定義変数、未使用変数、型不一致など、様々な種類の問題を検出し、エディタに報告します。

### このプログラムの役割

- **呼び出し元**: LSPプロトコルの `textDocument/publishDiagnostics` 関連処理
- **主な処理**: 各種診断モジュールを順番に実行し、検出された問題を収集
- **出力**: 診断結果のリスト（位置、メッセージ、重大度など）

## 2. ファイル基本情報

| 項目 | 内容 |
|-----|------|
| ファイルパス | `script/core/diagnostics/init.lua` |
| ファイル名 | init.lua |
| 種別 | ソースコード |
| 使用言語 | Lua |
| 総行数 | 192行 |
| 作成目的 | 診断機能の統括・実行管理を提供する |

## 3. 用語解説

このプログラムを理解するために必要な専門用語を解説します。

| 用語 | 読み方 | 意味・説明 |
|-----|--------|----------|
| Diagnostic | ダイアグノスティック | 診断。コードの問題を検出・報告する機能 |
| Severity | セベリティ | 重大度。Error, Warning, Information, Hint の4段階 |
| Workspace Diagnostic | ワークスペースダイアグノスティック | ワークスペース全体に対する診断 |
| File Status | ファイルステータス | 診断の対象ファイル条件（Any, Opened, None） |

### 言語固有の概念

- **---@async**: 非同期関数であることを示すLuaDocアノテーション
- **require による動的読み込み**: `require('core.diagnostics.' .. name)` で診断モジュールを動的に読み込む

## 4. 依存関係

### このプログラムが使用している外部ファイル

```
init.lua
    │
    ├─ require 'files' ............. ファイル管理
    │
    ├─ require 'proto.define' ...... 診断定義（重大度、ステータス等）
    │
    ├─ require 'config' ............ 設定管理
    │
    ├─ require 'await' ............. 非同期処理
    │
    ├─ require 'vm.vm' ............. 仮想マシン（診断無効化チェック）
    │
    ├─ require 'utility' ........... ユーティリティ
    │
    ├─ require 'proto.diagnostic' .. 診断グループ定義
    │
    └─ script/core/diagnostics/*.lua ... 各診断モジュール
```

| 参照ファイル | パス | 用途 |
|------------|------|------|
| files | `script/files.lua` | ファイルのAST取得、オープン状態確認 |
| define | `script/proto/define.lua` | 診断の重大度、ステータスの定義値 |
| config | `script/config/` | 診断設定の取得 |
| await | `script/await.lua` | 非同期処理（遅延、スリープ） |
| vm.vm | `script/vm/vm.lua` | 診断無効化チェック |

## 5. データ構造詳細

### 主要な変数・データ項目

#### 診断結果オブジェクト（check関数のresponseに渡される）

| 項目名 | 型 | 用途・説明 |
|-------|--------|----------|
| start | integer | 問題の開始位置 |
| finish | integer | 問題の終了位置 |
| message | string | 診断メッセージ |
| level | integer | 重大度レベル |
| code | string | 診断コード（例: 'undefined-global'） |
| data | any | 追加データ（コードアクション用など） |

#### diagCosts / diagCount テーブル（149-150行目）

| 項目名 | 型 | 用途・説明 |
|-------|--------|----------|
| diagCosts | table | 各診断の累計実行時間 |
| diagCount | table | 各診断の実行回数 |

**解説**: 診断のパフォーマンスを追跡し、次回の実行順序を最適化するために使用されます。

## 6. 処理詳細解説

このプログラムの処理内容をステップバイステップで説明します。

### 6.1 処理一覧

| No | 処理名 | 行番号 | 概要 |
|----|-------|--------|------|
| 1 | checkSleep | 12-32 | 診断速度制御のためのスリープ |
| 2 | getSeverity | 37-61 | 診断の重大度を取得 |
| 3 | getStatus | 66-90 | 診断のファイルステータスを取得 |
| 4 | check | 99-146 | 個別の診断を実行 |
| 5 | buildDiagList | 151-164 | 診断リストを構築・ソート |
| 6 | メイン関数 | 172-191 | 全診断を実行 |

### 6.2 checkSleep の詳細（12-32行目）

**この処理は何をするものか**: ワークスペース診断時に、CPU使用率を制御するためのスリープを挿入します。

**処理の流れ**:

```
ステップ1: 設定から速度率を取得
    │
    ↓
ステップ2: スリープ時間を計算
    │
    ↓
ステップ3: 必要に応じてスリープ
    │
    ↓
ステップ4: 実際のスリープ時間を記録
```

**ステップバイステップ解説**:

1. **ステップ1: 速度率の取得**（14-16行目）

   ```lua
   local speedRate = config.get(uri, 'Lua.diagnostics.workspaceRate')
   if speedRate <= 0 or speedRate >= 100 then
       return
   end
   ```

   `workspaceRate` 設定（0-100%）を取得します。0以下または100以上なら制限なしで終了します。

2. **ステップ2-4: スリープ時間の計算と実行**（17-31行目）

   ```lua
   local sleepTime = passed * (100 - speedRate) / speedRate
   -- ...
   await.sleep(sleepTime)
   ```

   経過時間に基づいてスリープ時間を計算し、`await.sleep` で実行します。

**この処理のポイント**:
- `sleepRest` 変数で細かいスリープ時間を蓄積し、効率的にスリープ
- 最大スリープ時間は 0.1 秒に制限

### 6.3 getSeverity の詳細（37-61行目）

**この処理は何をするものか**: 特定の診断の重大度（Error, Warning など）を取得します。

**処理の流れ**:

```
ステップ1: 個別設定を確認
    │
    ↓（強制設定の場合）
ステップ2: そのまま返す
    │
    ↓（強制でない場合）
ステップ3: グループ設定を確認
    │
    ↓
ステップ4: 最も重要な設定を返す
```

**ステップバイステップ解説**:

1. **ステップ1-2: 個別設定の確認**（38-42行目）

   ```lua
   local severity =   config.get(uri, 'Lua.diagnostics.severity')[name]
                   or define.DiagnosticDefaultSeverity[name]
   if severity:sub(-1) == '!' then
       return severity:sub(1, -2)
   end
   ```

   診断名に対する個別設定を確認します。末尾に `!` がある場合は強制設定として、グループ設定を無視します。

2. **ステップ3-4: グループ設定の確認**（43-60行目）

   ```lua
   local groupSeverity = config.get(uri, 'Lua.diagnostics.groupSeverity')
   local groups = diagd.getGroups(name)
   local groupLevel = 999
   for _, groupName in ipairs(groups) do
       local gseverity = groupSeverity[groupName]
       if gseverity and gseverity ~= 'Fallback' then
           groupLevel = math.min(groupLevel, define.DiagnosticSeverity[gseverity])
       end
   end
   ```

   診断が属するグループの設定を確認し、最も低いレベル（= 最も重要）を採用します。

**この処理のポイント**:
- 診断はグループに属することができ、グループ単位で重大度を設定可能
- `!` サフィックスで個別設定を強制できる

### 6.4 check の詳細（99-146行目）

**この処理は何をするものか**: 個別の診断モジュールを実行し、結果を収集します。

**処理の流れ**:

```
ステップ1: 診断が無効化されていないか確認
    │
    ↓
ステップ2: 重大度とステータスを取得
    │
    ↓
ステップ3: ファイルのオープン状態を確認
    │
    ↓
ステップ4: 診断モジュールを実行
    │
    ↓
ステップ5: 結果をフィルタリングして返す
```

**ステップバイステップ解説**:

1. **ステップ1: 無効化チェック**（100-103行目）

   ```lua
   local disables = config.get(uri, 'Lua.diagnostics.disable')
   if util.arrayHas(disables, name) then
       return false
   end
   ```

   `Lua.diagnostics.disable` 設定で無効化されている場合はスキップします。

2. **ステップ2-3: ステータスの確認**（104-113行目）

   ```lua
   local severity = getSeverity(uri, name)
   local status   = getStatus(uri, name)

   if status == 'None' then
       return false
   end

   if not ignoreFileOpenState and status == 'Opened' and not files.isOpen(uri) then
       return false
   end
   ```

   - ステータスが `None` なら実行しない
   - ステータスが `Opened` で、ファイルが開いていなければ実行しない

3. **ステップ4: 診断モジュールの実行**（115-134行目）

   ```lua
   require('core.diagnostics.' .. name)(uri, function (result)
       if vm.isDiagDisabledAt(uri, result.start, name) then
           return
       end
       if result.start < 0 then
           return
       end
       if mark[result.start] then
           return
       end
       mark[result.start] = true

       result.level = level or result.level
       result.code  = name
       response(result)
   end, name)
   ```

   - 診断モジュールを動的に読み込んで実行
   - `---@diagnostic disable` で無効化されている位置はスキップ
   - 同じ位置の重複診断はスキップ
   - 結果にレベルとコードを設定して返す

4. **ステップ5: パフォーマンス記録**（135-145行目）

   実行時間を記録し、0.5秒以上かかった場合は警告ログを出力します。

**この処理のポイント**:
- `require('core.diagnostics.' .. name)` で診断モジュールを動的に読み込む
- `mark` テーブルで同一位置の重複診断を防止
- `vm.isDiagDisabledAt` で `---@diagnostic` アノテーションをチェック

### 6.5 buildDiagList の詳細（151-164行目）

**この処理は何をするものか**: 診断リストを構築し、実行時間に基づいてソートします。

**ステップバイステップ解説**:

```lua
local function buildDiagList()
    if not diagList then
        diagList = {}
        for name in pairs(define.DiagnosticDefaultSeverity) do
            diagList[#diagList+1] = name
        end
    end
    table.sort(diagList, function (a, b)
        local time1 = (diagCosts[a] or 0) / (diagCount[a] or 1)
        local time2 = (diagCosts[b] or 0) / (diagCount[b] or 1)
        return time1 < time2
    end)
    return diagList
end
```

- 初回は全診断名をリストに追加
- 平均実行時間でソート（速い診断を先に実行）

**この処理のポイント**:
- 速い診断を先に実行することで、早期にフィードバックを提供
- 累計時間 / 実行回数 で平均実行時間を計算

### 6.6 メイン関数の詳細（172-191行目）

**この処理は何をするものか**: 全ての診断を順番に実行し、結果を収集します。

**処理の流れ**:

```
ステップ1: ASTを取得
    │
    ↓
ステップ2: 診断リストを取得
    │
    ↓
ステップ3: 各診断を実行
    │
    ↓
ステップ4: 結果を報告
```

**ステップバイステップ解説**:

```lua
return function (uri, isScopeDiag, response, checked, ignoreFileOpenState)
    local ast = files.getState(uri)
    if not ast then
        return nil
    end

    for _, name in ipairs(buildDiagList()) do
        await.delay()
        local clock = os.clock()
        local suc = check(uri, name, isScopeDiag, response, ignoreFileOpenState)
        if suc then
            local cost = os.clock() - clock
            diagCosts[name] = (diagCosts[name] or 0) + cost
            diagCount[name] = (diagCount[name] or 0) + 1
        end
        if checked then
            checked(name)
        end
    end
end
```

1. ASTが存在しなければ終了
2. ソートされた診断リストを取得
3. 各診断を実行し、時間を記録
4. `checked` コールバックで進捗を報告

**この処理のポイント**:
- `await.delay()` で各診断の前に制御を渡し、UIのフリーズを防止
- 実行時間を追跡し、次回のソートに使用

## 7. 処理フロー図

プログラム全体の処理の流れを図で示します。

```mermaid
flowchart TD
    A[診断開始] --> B[AST取得]
    B --> C{AST存在?}
    C -->|No| Z[終了]
    C -->|Yes| D[診断リストをソート]
    D --> E[次の診断を取得]
    E --> F{全診断完了?}
    F -->|Yes| Z
    F -->|No| G[await.delay]
    G --> H{診断が有効?}
    H -->|No| E
    H -->|Yes| I[診断モジュールを実行]
    I --> J[結果をフィルタリング]
    J --> K[responseに結果を渡す]
    K --> L[実行時間を記録]
    L --> E
```

**図の説明**:
- **診断リストをソート**: 実行時間が短い診断を先に実行
- **診断が有効?**: 無効化設定、ステータス、ファイルのオープン状態をチェック
- **結果をフィルタリング**: 重複、無効化位置を除外

## 8. 外部連携

### 呼び出し関係図

```
[呼び出し元]                    [このプログラム]                    [呼び出し先]
                                     │
provider.lua ──────────────▶ diagnostics/init.lua ─────────▶ diagnostics/*.lua
  (publishDiagnostics)               │                          (各診断モジュール)
                                     │
                                     ├──────────────▶ vm.isDiagDisabledAt()
                                     │                   (無効化チェック)
                                     │
                                     └──────────────▶ config.get()
                                                        (設定取得)
```

### 呼び出し元（このプログラムを呼び出すもの）

| プログラム名 | 呼び出し契機 | 渡されるデータ |
|------------|------------|--------------|
| provider.lua | ファイルの変更時、定期的な更新時 | URI, isScopeDiag, response, checked |

### 呼び出し先（このプログラムが呼び出すもの）

| プログラム名 | 呼び出し目的 | 渡すデータ | 受け取るデータ |
|------------|------------|----------|--------------|
| diagnostics/*.lua | 各種診断の実行 | URI, コールバック | 診断結果 |
| vm.isDiagDisabledAt | 位置ごとの無効化チェック | URI, 位置, 名前 | boolean |
| config.get | 設定値の取得 | URI, キー | 設定値 |

## 9. エラー処理

このプログラムで発生する可能性があるエラーと、その対処方法を説明します。

### エラー一覧

| エラー種別 | 発生条件 | 対処内容 | 該当行 |
|----------|---------|---------|--------|
| ASTなし | ファイルが解析されていない | null を返す | 173-176 |
| 診断無効 | 設定で無効化されている | その診断をスキップ | 100-103 |
| 重複診断 | 同じ位置に複数の診断 | 2つ目以降をスキップ | 126-129 |

### エラー発生時の動作

このプログラムは例外を投げません。診断に失敗した場合はその診断をスキップし、他の診断は継続して実行されます。

## 10. このプログラムを読むときのポイント

このプログラムを理解するためのアドバイスです。

### 推奨する読み方

1. **まずメイン関数（172-191行目）を見る**: 全体の流れを把握
2. **次に check 関数を見る**: 個別診断の実行方法を理解
3. **最後に getSeverity/getStatus を見る**: 設定システムを理解

### 注意すべきポイント

- `require('core.diagnostics.' .. name)` で診断モジュールを動的に読み込みます
- 診断の実行順序は平均実行時間でソートされます
- `!` サフィックスで設定を強制できます
- `---@diagnostic` アノテーションで診断を無効化できます

### 関連して読むべきファイル

| ファイル | パス | 関連性 |
|---------|------|--------|
| undefined-global.lua | `script/core/diagnostics/undefined-global.lua` | 診断モジュールの実装例 |
| define.lua | `script/proto/define.lua` | 重大度、ステータスの定義 |
| config/ | `script/config/` | 診断設定の管理 |

## 11. 備考

- この init.lua は診断の「統括」を行い、実際の診断ロジックは各サブモジュールにあります
- 診断モジュールは 60 以上あり、様々な問題を検出します
- ワークスペース診断時は `workspaceRate` 設定でCPU使用率を制御できます
- 診断グループにより、関連する診断を一括で設定できます

### 主な診断モジュール

| モジュール | 検出する問題 |
|-----------|-------------|
| undefined-global | 未定義のグローバル変数 |
| unused-local | 未使用のローカル変数 |
| unused-function | 未使用の関数 |
| duplicate-index | テーブルの重複キー |
| assign-type-mismatch | 型の不一致 |
| deprecated | 非推奨APIの使用 |
| spell-check | スペルミス |
| trailing-space | 行末の空白 |
