GitHub Copilotがコードレビューまでしてくれるようになったので試してみた【GitHub dockyard】

本記事は[GitHub dockyard Advent Calendar 2024](https://qiita.com/advent-calendar/2024/github-dockyard)の11日目の記事です

# はじめに
こんにちは！大学生でWebフロントエンドエンジニアをしている、hibikiです:-)

VS Codeの10月リリースでは、GitHub Copilotについて多くの新機能が追加されました
[GitHub Copilotの新機能についてのチェンジログ](https://github.blog/changelog/2024-10-29-multi-file-editing-code-review-custom-instructions-and-more-for-github-copilot-in-vs-code-october-release-v0-22/)

上の記事では、見出しの機能として`Copilot Edits`が挙げられています
`Copilot Edits`に関する記事は同じくGitHub dockyard Advent Calendar 2024で@youtoyさんが執筆されている、[GitHub Copilot の「Copilot Edits」で直接の部分的なコード書きかえを行ってもらう（機能の有効化の話も）【GitHub dockyard】](https://qiita.com/youtoy/items/5249b5f7a1acabda5f01)をご覧いただければと思います

ここでは、同じく新機能として追加された`Code Review(Preview)`についてReact+Viteのテストプロジェクトで試してみました

# え！GitHub Copilotがコードレビューまで！？👀
GitHub Copilotの進化は凄まじく、質問に答えてくれるだけでなく複数ファイルの編集もしてくれるようになりました
実際に先月、Copilot EditsでFlutterやReactプロジェクトを爆速で形に仕上げることができました。ファイル同士のコンテキストを汲み取ってくれるのでアプリ開発に大変便利です

そして、遂にはコードレビューまでしてくれるようです
上記のチェンジログには次のように書いてあります
> With Copilot-powered code review in Visual Studio Code, you can now get fast, AI-powered feedback on your code as you write it, or request a review of all your changes before you push. Code review in Visual Studio Code is currently in preview. Try it out and provide feedback through our issues.

Copilotが自分の書いたコードに対してフィードバック、レビューを返してくれます！
それもCIのようにプッシュ後ではなくローカル上で任意のタイミングで実行することができます
これなら書いている途中でもコードを改善できますし、ブランチの状態をきれいに保つことにも繋がります。そしてなにより、レビュワーの負担を減らせます！
**開発生産性⤴⤴**

# How to Use
チェンジログで挙げられているレビュー方法は2つあります
1. **選択内容のレビュー**
こちらはカーソルで選択した箇所のコードをレビューしてくれます、コンテキストメニューもしくはコマンドパレットから実行可能
1. **変更内容のレビュー**(ウェイトリストに入っておく必要あり)
コミットしていないすべての変更内容についてより詳しくレビューをしてくれます（GitHub上のプルリクエストで実行できるものと同様）、「ソース管理」メニューにある「Copilot Code Review」ボタンから実行可能

今回は[ウェイトリスト](https://github.com/github-copilot/code-review-waitlist)に入っていないため、「選択内容のレビュー」のみについて取り上げますmm

## コンテキストメニュー
![VS Codeのコンテキストメニューを"Copilot → レビューとコメント"と進めている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/36240050-38f6-c273-025e-f4c48806a23e.png)

## コマンドパレット
![コマンドパレットで"> copilot"と入力すると"GitHub Copilot: レビューとコメント"という表示が出ている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/178002c0-d99a-edf1-1af0-bced05b5dfa7.png)
カーソルを関数内に置いてコマンドパレットから実行すると、ファイル内の階層ごとに選択してくれます
（addTodoの内部、newTodo変数の宣言など）
!["囲む範囲を選択してレビューする"という表示の下にnewTodo、addTodo、Appを選択できるメニューが表示されている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/a7038cfb-06d9-1369-9424-b43275e28654.png)

# 実際にReact+Viteプロジェクトで試してみた
それでは、実際に試していきましょう
今回はシングルページでTodoリストを作成しました
```tsx:App.tsx
import { useState } from 'react'
import './index.css'

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

function App() {
  // 状態の分割管理（本来はuseReducerを使うべき）
  const [todos, setTodos] = useState<Todo[]>([]);
  const [inputText, setInputText] = useState('');
  const [searchText, setSearchText] = useState('');
  const [isEditing, setIsEditing] = useState(false);
  const [editId, setEditId] = useState<number | null>(null);
  const [editText, setEditText] = useState('');
  const [filterType, setFilterType] = useState<'all' | 'active' | 'completed'>('all');

  // イベントハンドラの型安全性が欠如
  const addTodo = (e: any) => {
    if (inputText.trim() === '') return;
    const newTodo: Todo = {
      id: Date.now(),
      text: inputText,
      completed: false
    };
    setTodos([...todos, newTodo]);
    setInputText('');
  };

  const toggleTodo = (id: number) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  // 非効率な実装（毎回新しい配列を生成）
  const getFilteredAndSortedTodos = () => {
    let result = [...todos];
    if (filterType === 'active') {
      result = result.filter(todo => !todo.completed);
    } else if (filterType === 'completed') {
      result = result.filter(todo => todo.completed);
    }
    
    result = result.filter(todo =>
      todo.text.toLowerCase().includes(searchText.toLowerCase())
    );

    return result.sort((a, b) => b.id - a.id);
  };

  // メモ化されていない計算プロパティ
  const stats = {
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    remaining: todos.filter(t => !t.completed).length
  };

  const filteredTodos = getFilteredAndSortedTodos();

  return (
    <div className="w-full h-full p-8">
      <div style={{ width: '100%', maxWidth: '42rem', margin: '0 auto', backgroundColor: 'white', borderRadius: '0.5rem', borderWidth: '2px' }}>
        <div className="p-4 border-b-2">
          <input
            className="w-full h-12 px-4 mb-4 border-2 rounded-lg"
            placeholder="検索..."
            value={searchText}
            onChange={(e) => setSearchText(e.target.value)}
          />
          <div className="flex gap-2 mb-4">
            <button
              className={`px-4 py-2 rounded ${filterType === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
              onClick={() => setFilterType('all')}
            >
              全て
            </button>
            <button
              className={`px-4 py-2 rounded ${filterType === 'active' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
              onClick={() => setFilterType('active')}
            >
              未完了
            </button>
            <button
              className={`px-4 py-2 rounded ${filterType === 'completed' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
              onClick={() => setFilterType('completed')}
            >
              完了済み
            </button>
          </div>
          <div className="flex gap-2">
            <input
              className="flex-1 h-12 px-4 border-2 rounded-lg"
              placeholder="新しいタスク"
              value={inputText}
              onChange={(e) => setInputText(e.target.value)}
              // コールバック関数の不適切な定義（レンダリングごとに新しい関数が作成される）
              onKeyPress={function(e: React.KeyboardEvent<HTMLInputElement>) {
                if (e.key === 'Enter') {
                  addTodo(e)
                }
              }}
            />
            <button
              style={{ padding: '0 1.5rem', backgroundColor: '#3b82f6', color: 'white', borderRadius: '0.5rem' }}
              onClick={addTodo}
            >
              追加
            </button>
          </div>
        </div>
        <div className="p-4 border-b-2 text-sm text-gray-500">
          全て: {stats.total} | 完了: {stats.completed} | 残り: {stats.remaining}
        </div>
        <ul className="divide-y">
          {filteredTodos.map(todo => (
            <li
              key={todo.id}
              className="flex items-center p-4 hover:bg-gray-50 cursor-pointer"
              onClick={() => toggleTodo(todo.id)}
            >
              <input
                type="checkbox"
                checked={todo.completed}
                className="w-5 h-5 mr-4"
                readOnly
              />
              <span className={`flex-1 ${todo.completed ? 'line-through text-gray-400' : ''}`}>
                {todo.text}
              </span>
            </li>
          ))}
        </ul>
      </div>
    </div>
  )
}

export default App
```
UIライブラリは普段使用しているtailwindCSSを使用しています
ところどころに修正すべき点を加えて(正常に動作はする)、ちゃんとレビューをしているかを確かめます

## ちゃんとレビューしてくれた！でも...
<!-- 同じコードでもコードレビューをするときとしないときがある -->
<!-- 問題点をコメントアウトしているため拾えているだけの可能性 -->

![GitHub Copilotが`addTodo`関数の引数`e`についての問題点と変更の提案を提示している](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/d59eb127-80b2-0f3b-ca5e-54e168ca428e.png)
Code Reviewを走らせると上の画像のようにレビューと変更の提案をしてくれます！
提案までしてくれるのはありがたいですね✨️

この実行では他のコメントアウトしている5箇所のほとんど(`getFilteredAndSortedTodos`以外)でレビューをしてくれました

しかし、何度か試してみると、この機能はかなりばらつきがあるのかなと感じました。
というのも、同じコードでもレビューの箇所を検出できるときとできないときがあり（大半は検出できていない）、またコメントを削除すると実行してもほとんど検出されていませんでした
これはまだプレビューの機能であるため、今後の改善に期待したいところです🌈

# まとめ
今回は、GitHub Copilotに追加された新機能`Code Review(Preview)`について、テストプロジェクトで試してみました！
現段階では精度に課題がありますが、今後の改善によって開発者とレビュワー双方の負担を軽減し、開発生産性の~~工場~~向上が期待できると感じました！
今後も引き続き情報を追っていこうと思います👀

それでは、引き続き[GitHub dockeryard Advent Calendar 2024](https://qiita.com/advent-calendar/2024/github-dockyard)をお楽しみください！
ご清覧ありがとうございました🙇‍♂️