連載|静的解析×AIエージェントを始めよう! 第2回 GitHub Copilot×SARIFの留意点(前編)

前回のコラム「静的解析×AIエージェントをはじめよう! 第1回 SARIFを使う」では、SARIF Viewerで表示した静的解析の指摘に対して、VS Code上でGitHub Copilot Chatの Explain(説明)や Fix(修正)を実行し、静的解析結果とAIエージェントを組み合わせる感覚をつかむところまでを紹介し、最後に、次のような注意書きを添えていました。

ただし、これらの機能を通して行う推論(説明や修正案)には制約があります。この点については、今後のコラムで別途説明いたします。

本稿では、この「制約」の正体を説明します。

Artist's impression of a multi-core CPU

この連載の記事一覧

連載|静的解析×AIエージェントを始めよう!

はじめに

結論を先に言うと、SARIF Viewerで取り込んだSARIFの情報は、そのすべてがGitHub Copilot ChatのFix/Explainに渡っているわけではありません。SARIFには、指摘メッセージだけでなく、複数の関連位置や「原因から発生まで」のCodeFlowなど多くの情報を持たせることができます。しかし実際にモデルへ渡る診断情報は、経路によって差はあるものの、中心になるのは診断メッセージと代表位置です。Fixの一部の経路では代表位置周辺のコードも含まれますが、SARIFのCodeFlowや2番目以降のlocationsがそのまま渡るわけではありません。

この絞り込みがどこで起きているのかを、SARIF Viewer → VS Code本体 → GitHub Copilot Chatという流れに沿って、SARIF Viewer拡張機能・VS Code本体・GitHub Copilot Chat拡張機能という3つの実際のソースコードを引用しながら確認していきます。

前回のおさらい:null_ptr.c

前回のコラムでは、Cppcheckで以下のコードを解析し、SARIF形式で出力しました。

#include <stddef.h>
int main(void)
{
    int *ptr = NULL;
    return *ptr;
}

このときのSARIFのresults[ ]には、nullPointerという1件の指摘に対して、locations[ ]に2つの位置情報が入っていました。

{
  "version": "2.1.0",
  "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
  "runs": [
    {
      "results": [
        {
          "level": "error",
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": {
                  "uri": "null_ptr.c"
                },
                "region": {
                  "startLine": 5,
                  "startColumn": 16,
                  "endLine": 5,
                  "endColumn": 16
                }
              }
            },
            {
              "physicalLocation": {
                "artifactLocation": {
                  "uri": "null_ptr.c"
                },
                "region": {
                  "startLine": 7,
                  "startColumn": 13,
                  "endLine": 7,
                  "endColumn": 13
                }
              }
            }
          ],
          "message": {
            "text": "Null pointer dereference: ptr"
          },
          "partialFingerprints": {
            "hash/v1": "5628042425796901966"
          },
          "ruleId": "nullPointer"
        }
      ],
      "tool": {
        "driver": {
          "informationUri": "https://cppcheck.sourceforge.io",
          "name": "Cppcheck",
          "semanticVersion": "2.22"
        }
      }
    }
  ]
}
  • 5行目:ptrにNULLを代入している箇所
  • 7行目:そのptrを参照してNULLポインタ参照が発生する箇所

このCppcheckの出力では、5行目と7行目の2地点がlocations[ ]に入っています。人が見れば「NULLを代入した箇所」と「そのNULLを参照した箇所」という関係を読み取れます。

注意

SARIFのresults[ ].locations[ ]は、各要素が「その result が発生した位置」を表すためのフィールドです。実行経路の詳細を表現する用途にはcodeFlows[ ]が用意されており、その中のthreadFlows[ ].locations[ ]には、問題を示す実行順序に従って位置を記録します。また、result そのものの発生位置ではないものの、その result を理解するうえで関連する位置はrelatedLocations[ ]で表現できます。

仕様上、locations[ ]が複数の要素を持てるのは、その指摘が「配列中のすべての位置を直さなければ解消しない」場合に限られます。OASISのSARIF仕様にも以下のように書かれています。

The locations array SHALL NOT contain more than one element unless the condition indicated by the result, if any, can only be corrected by making a change at every location specified in the array.

上記のCppcheckの出力のように原因と発生を並べる使い方は、この想定から外れたものです。

出典:OASIS:Static Analysis Results Interchange Format (SARIF) V2.1.0

このSARIFをVS Codeで表示すると、以下のような状態になります。

この状態でPROBLEMSパネル上の指摘を右クリックしてExplainやFixを実行したとき、GitHub Copilotは「5行目でNULLが代入され、7行目で参照されて問題になる」という流れそのものを理解した上で説明・修正しているのでしょうか?

調査方法:GitHub Copilot ChatとSARIF ViewerはどちらもOSS

GitHub Copilot Chat拡張機能そのもののソースコードは、かつてはmicrosoft/vscode-copilot-chatという単独のリポジトリでOSS公開されていました。ただしこのリポジトリは2026年5月20日付でオーナーによりアーカイブ(read-only化)されており、README上でも「本プロジェクトはVS Code本体のリポジトリ(microsoft/vscode)に統合され、以降の開発・Issue/PRはそちらで行う」旨が案内されています。実際、現在Copilot Chatのソースコードはmicrosoft/vscodeリポジトリのextensions/copilot/以下に置かれています。

そのため、本稿で引用するCopilot Chat側のコードは、2026年8月時点のmicrosoft/vscodeのmainブランチ(extensions/copilot/以下)を基準にしています。同じmainブランチには、VS Code本体側のコード(拡張機能ホストの実装など)も含まれているので、そちらも同じ基準で引用します。mainは日々更新される生きたブランチなので、実際のコードは読んでいるタイミングによってさらに変わっている可能性があります。なお、実際に動かして挙動を確認した環境は、VS Code 1.134.0(コミット110a328e)です。

本稿の前提

ここからは、ローカルのVS Code DesktopでSARIF ViewerとGitHub Copilot Chatが同一のExtension Host上で動作する一般的な構成を前提に説明します。Remote DevelopmentやWeb版VS Codeなど、Extension Hostが分離される構成ではDiagnosticの受け渡し経路が異なる場合がありますが、本稿では扱いません。

同様に、SARIF ViewerもOSSであり、microsoft/sarif-vscode-extensionで公開されています。こちらはGitHubのコミット履歴上で2025年6月11日付と表示されているbd3eeb1時点のコードを引用します。

これらのコードを見ながら、「SARIFの指摘は、どうやって1つのDiagnosticインスタンスに集約されるのか」、「そのインスタンスに、SARIF Viewerはどこまで詰めているのか」、「そのインスタンスを、PROBLEMSパネルとGitHub Copilot Chatはそれぞれどう読んでいるのか」という3段階に分けて、実際のコードを追ってみます。

ステップ1:SARIFの指摘は、どんな「箱」に入れられるのか

まず前提として、VS Codeの拡張機能APIには、検出した指摘(診断)を表現するvscode.Diagnosticというクラスが用意されています。言語機能拡張やLint拡張、静的解析ビューアなどは、指摘をこのクラスのインスタンスとして組み立て、DiagnosticCollection.set()でVS Codeの診断機構へ登録します。あとはVS Code本体が、その内容をPROBLEMSパネルやエディタ上の波線として表示します。SARIF Viewerも、この仕組みに従っています。SARIFファイルを読み込むだけでなく、SARIFのresultを、vscode.Diagnosticを継承したResultDiagnosticへ変換して登録しています。

SARIF ViewerがSARIFのresultをDiagnosticに変換している箇所が、以下のコードです。

// src/extension/index.ts (microsoft/sarif-vscode-extension)
const diags = matchingResults
    .map(result => {
        return new ResultDiagnostic(
            driftedRegionToSelection(diffBlocks, currentDoc, result._region, originalDoc),
            result._message ?? '—',
            severities[result.level ?? ''] ?? DiagnosticSeverity.Information, // note, none, undefined.
            result,
        );
    });

diagsAll.set(doc.uri, diags);

https://github.com/microsoft/sarif-vscode-extension/.../index.ts

ResultDiagnosticクラスの定義は次の通りです。VS Code標準のDiagnosticクラスを継承し、resultプロパティを1つ追加しただけのクラスです。

// src/extension/resultDiagnostic.ts (microsoft/sarif-vscode-extension)
export class ResultDiagnostic extends Diagnostic {
    constructor(range: Range, message: string, severity: DiagnosticSeverity, readonly result: Result) {
        super(range, message, severity);
    }
}

https://github.com/microsoft/sarif-vscode-extension/.../resultDiagnostic.ts

コンストラクタ引数のreadonly result: Resultによって、ResultDiagnosticのインスタンスには、変換元のSARIFのresultオブジェクトが.resultとして保持されます。継承元になっているVS Code標準のDiagnosticクラス自体が、そもそもどんなフィールドを持っているのかを確認しておきます。次のコードは、拡張機能からvscode.Diagnosticとして見えるクラスの実体(VS Code本体側の実装)です。なお、拡張機能向けに公開されている型宣言そのものはvscode.d.tsにあります。

// src/vs/workbench/api/common/extHostTypes/diagnostic.ts (microsoft/vscode)
export class Diagnostic {

    range: Range;
    message: string;
    severity: DiagnosticSeverity;
    source?: string;
    code?: string | number;
    relatedInformation?: DiagnosticRelatedInformation[];
    tags?: DiagnosticTag[];

    constructor(range: Range, message: string, severity: DiagnosticSeverity = DiagnosticSeverity.Error) {
        if (!Range.isRange(range)) {
            throw new TypeError('range must be set');
        }
        if (!message) {
            throw new TypeError('message must be set');
        }
        this.range = range;
        this.message = message;
        this.severity = severity;
    }
    // ...
}

https://github.com/microsoft/vscode/.../diagnostic.ts

つまりDiagnosticクラスが持つフィールドは、range・message・severity・source・code・relatedInformation・tagsの7つであり(codeの型は、vscode.d.tsではstring | number | { value: string | number, target: Uri }と宣言されています)、後述するcodeFlowsのような構造をそのままの形で受け取れるフィールドは、クラス定義そのものに存在しません。ResultDiagnosticがsuper(...)で渡せるのも、親クラスのコンストラクタが受け取るrange・message・severityの3つだけです。残るsource・code・relatedInformation・tagsは任意フィールドなので、インスタンス生成後に代入することもできますが、SARIF Viewerはそれも行っていません(ステップ2で確認します)。

そしてdiagsAllが何者かというと、同じファイルの先頭側で次のように宣言されています。

// src/extension/index.ts (microsoft/sarif-vscode-extension)
const diagsAll = languages.createDiagnosticCollection('SARIF');

languages.createDiagnosticCollection('SARIF')は、VS Code拡張機能APIが提供するDiagnosticCollectionを作る関数です。つまりdiagsAll.set(doc.uri, diags)は、SARIF Viewerが独自に用意した特別な経路ではありません。言語機能拡張やLint拡張も、まったく同じDiagnosticCollection.set()というAPIで指摘を登録しています。

これによって、SARIF Viewerが作ったResultDiagnosticはVS Code標準の診断機構へ渡されます。少なくともPROBLEMSパネル上の表示と、PROBLEMSパネルやエディタ上の右クリック・Quick Fixから起動するExplain/Fixは、この診断機構を通して指摘を扱います。つまり、SARIF ViewerがResultDiagnosticにどの情報を載せたかが、その後に利用できる情報の出発点になります。

次のステップ2では、SARIF ViewerがSARIFの情報をResultDiagnosticへ変換する際、実際に何を載せ、何を載せていないのかを見ていきます。

ステップ2:SARIF Viewerは、その「箱」にどこまで詰めているのか

ResultDiagnosticはVS Code標準のDiagnosticを継承しているだけなので、理論上はrange・message・severity・relatedInformationといった標準フィールドを、SARIFの情報でできるだけ豊かに埋めることもできたはずです。しかし実際のSARIF Viewerの実装を見ると、かなり手前の段階で情報が絞り込まれています。

前述の繰り返しとなりますが、SARIF ViewerがSARIFのresultをDiagnosticに変換している箇所が、以下のコードです。

// src/extension/index.ts (microsoft/sarif-vscode-extension)
const diags = matchingResults
    .map(result => {
        return new ResultDiagnostic(
            driftedRegionToSelection(diffBlocks, currentDoc, result._region, originalDoc),
            result._message ?? '—',
            severities[result.level ?? ''] ?? DiagnosticSeverity.Information, // note, none, undefined.
            result,
        );
    });

diagsAll.set(doc.uri, diags);

まず、range(_region)を作る処理を見てみます。使っているのは常に先頭の要素だけです。

// src/shared/index.ts (microsoft/sarif-vscode-extension)
const ploc = result.locations?.[0]?.physicalLocation;
// ...
result._region = ploc?.region;

https://github.com/microsoft/sarif-vscode-extension/.../index.ts

前述の通り、Cppcheckが出力したSARIFにおけるNullポインタ参照に関する該当部分では、locations[ ]に2つの位置情報が入っていました。SARIFの出力としては、7行目も"locations"で出力されているにもかかわらず、5行目がPROBLEMSパネルの代表位置になっていたのは、このrangeを作る際に2番目以降が単純に無視されるのが理由です。Cppcheckのようにlocations[ ]だけを使うシンプルな出力であれば、話はここで一区切りです。2番目以降の位置情報は、この時点でDiagnosticの標準フィールドには反映されません。後述しますが、複数の関連位置を運べるrelatedInformationにも設定されていないため、ここで落ちたまま復元されることはありません。

では、前回のコラムで紹介したCodeSonarはどうでしょうか?

上記画像内ではStep 1、Step 2のような「原因箇所→発生箇所」という一連の流れが表示されていますが、これはCodeSonarのSARIFに埋め込んでいるcodeFlowsというフィールドの値を使っています。codeFlowsは、SARIFで非必須として定義されているフィールドです。ここからは、Cppcheckの出力には登場しないcodeFlowsを例に、SARIF Viewerの変換コードを確認します。

VS CodeのDiagnosticには、複数の関連情報を表現するためのrelatedInformationという標準フィールドが用意されています。もしSARIF ViewerがこのrelatedInformationにcodeFlowsの各ステップを詰めていれば、PROBLEMSパネルやGitHub Copilot Chatもその情報にアクセスできたはずです。しかし、bd3eeb1時点のリポジトリ全体(追跡ファイル97件)をrelatedInformationという文字列で検索しても、ヒットは1件もありません。これはMarketplaceで公開されている版(3.4.5)に対応するタグでも同じです。設定している箇所がないどころか、DiagnosticRelatedInformationという型をvscodeモジュールから取り込んでいる箇所すら存在しません。Diagnosticの標準フィールドのうちSARIF Viewerが値を入れているのはrange・message・severityの3つだけで、code・source・tagsへの代入も見つかりませんでした。

つまり、SARIFのcodeFlowsが持つ「原因箇所→発生箇所」という流れの情報は、SARIF ViewerがVS CodeのDiagnosticに変換する、この一番最初の瞬間から、すでに標準フィールド上には反映されていません。codeFlowsはresultオブジェクトの中にはそのまま残っていて、readonly result: Resultという独自プロパティとしてResultDiagnosticにぶら下がってはいます。ただしこれは、SARIF Viewer自身が自分のUI表示のために.resultとして掘り出すことを前提にした、いわば「私物」の持ち方であって、Diagnosticという共通の箱の中には入っていません。

これが、最も上流にある絞り込みです。PROBLEMSパネルやGitHub Copilot Chatが何を読むかという話の前に、そもそもSARIF Viewer自身が、共通の箱に詰める段階でlocations[0]しか使っておらず、locations[1..]もcodeFlowsも標準フィールドには載せていない、という点をまず押さえておきます。


この連載の記事一覧

連載|静的解析×AIエージェントを始めよう!

第1回でSARIFの基本を学び、第2回ではSARIF ViewerからVS Code Diagnosticへ変換される過程を追いました。SARIF ViewerがDiagnosticの標準フィールドとして設定しているのはrange・message・severityのみで、SARIFの複数locationsやcodeFlowsは標準フィールドには反映されないことが分かりました。

次回は、この情報の欠落がGitHub Copilot ChatのExplain/Fixの推論にどう影響するかを検証します。

このコラムの著者

株式会社ユビキタスAI

エンベデッド第3部 セールス&PM セクション

藤江 克彦​(ふじえ かつひこ)

より詳しく技術や​関連製品について​知りたい方へ

本コラムに関係する技術や関連する製品について知りたい方は、お気軽にご相談ください。


製品情報

高精度静的解析ツール

CodeSonar

世界トップレベルの解析能力で不具合や脆弱性の原因となるソースコードのバグを検出
製品ページを見る
連載

連載|静的解析×AIエージェントを始めよう! 第1回 SARIFを使う

コラムを読む

CWE-1000は脆弱性じゃない⁉ CWEが直感的に分かりにくい理由

コラムを読む
メニューを閉じる
一つ前に戻る