LESSON 25分

レビューチェックリスト

ストーリー

「マインドセットは分かったが、実際に何を見ればいいか分からない...... という状態だろう?」

松本先輩がチェックリストの書かれた資料を渡してくれた。

「レビューは『感覚』でやるものじゃない。 体系的なチェックリストに沿って確認することで、 見落としを防ぎ、一貫した品質を保てるんだ」

「このチェックリストを使えば、経験が浅くても 効果的なレビューができるようになる。一つずつ見ていこう」


レビューチェックリストの全体像

コードレビューでは、以下の7つの観点で確認を行います。

┌──────────────────────────────────────────┐
│           レビュー7つの観点              │
├──────────────────────────────────────────┤
│  1. 正確性(Correctness)               │
│  2. 設計(Design)                      │
│  3. 可読性(Readability)               │
│  4. セキュリティ(Security)             │
│  5. パフォーマンス(Performance)        │
│  6. テスト(Testing)                   │
│  7. ドキュメント(Documentation)        │
└──────────────────────────────────────────┘

1. 正確性(Correctness)

コードが意図した通りに動作するか確認します。

チェック項目

  • 要件を正しく満たしているか
  • エッジケースは考慮されているか
  • エラーハンドリングは適切か
  • null / undefined のチェックはあるか

例:エッジケースの見落とし

typescript
// レビュー対象のコード
function calculateDiscount(price: number, discountRate: number): number {
  return price * (1 - discountRate);
}

// レビューコメント:
// ⚠️ [Must Fix] エッジケースが考慮されていません
// - price が負の値の場合はどうなりますか?
// - discountRate が 0〜1 の範囲外の場合は?
// - discountRate が 1 を超えるとマイナスの価格になります

// 改善案
function calculateDiscount(price: number, discountRate: number): number {
  if (price < 0) {
    throw new Error('Price must be non-negative');
  }
  if (discountRate < 0 || discountRate > 1) {
    throw new Error('Discount rate must be between 0 and 1');
  }
  return Math.round(price * (1 - discountRate));
}

2. 設計(Design)

コードの構造と設計の妥当性を確認します。

チェック項目

  • 単一責任の原則(SRP)に従っているか
  • 適切な抽象化がされているか
  • 既存のコードベースと一貫性があるか
  • 過度な複雑さはないか(YAGNI)

例:責務の分離

typescript
// レビュー対象のコード
class UserService {
  async createUser(data: CreateUserDto) {
    // バリデーション
    if (!data.email.includes('@')) throw new Error('Invalid email');
    if (data.password.length < 8) throw new Error('Password too short');

    // DBに保存
    const user = await this.db.insert('users', data);

    // メール送信
    const transporter = nodemailer.createTransport({ /* ... */ });
    await transporter.sendMail({
      to: data.email,
      subject: 'Welcome!',
      html: '<h1>Welcome to our service!</h1>',
    });

    // ログ出力
    console.log(`User created: ${user.id}`);

    return user;
  }
}

// レビューコメント:
// ⚠️ [Should Fix] UserService がバリデーション、DB操作、
// メール送信、ログ出力の全てを担当しています。
// 単一責任の原則に基づいて分離することを提案します。

// 改善案
class UserService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly emailService: EmailService,
    private readonly logger: Logger,
  ) {}

  async createUser(data: CreateUserDto): Promise<User> {
    const email = Email.create(data.email);       // Value Object でバリデーション
    const password = Password.create(data.password);

    const user = await this.userRepository.save(
      User.create({ email, password })
    );

    await this.emailService.sendWelcome(user.email);
    this.logger.info(`User created: ${user.id}`);

    return user;
  }
}

3. 可読性(Readability)

コードが他の開発者にとって理解しやすいか確認します。

チェック項目

  • 変数名・関数名は意図を表しているか
  • ネストが深すぎないか(最大3階層が目安)
  • マジックナンバーはないか
  • 適切なコメントがあるか

例:命名とネスト

typescript
// レビュー対象のコード
function proc(d: any[]) {
  const r: any[] = [];
  for (let i = 0; i < d.length; i++) {
    if (d[i].s === 1) {
      if (d[i].a > 18) {
        if (d[i].t === 'premium') {
          r.push(d[i]);
        }
      }
    }
  }
  return r;
}

// レビューコメント:
// ⚠️ [Should Fix] 命名とネストの改善を提案します
// - 関数名・変数名が省略されすぎて意味が不明です
// - ネストが4階層と深く、読みにくくなっています
// - any 型は避けてください

// 改善案
interface User {
  status: number;
  age: number;
  tier: string;
}

function filterActivePremiumAdults(users: User[]): User[] {
  return users.filter(user =>
    user.status === UserStatus.ACTIVE &&
    user.age > MINIMUM_ADULT_AGE &&
    user.tier === 'premium'
  );
}

const UserStatus = { ACTIVE: 1, INACTIVE: 0 } as const;
const MINIMUM_ADULT_AGE = 18;

4. セキュリティ(Security)

セキュリティ上の問題がないか確認します。

チェック項目

  • SQLインジェクションの可能性はないか
  • XSS(クロスサイトスクリプティング)のリスクはないか
  • 認証・認可のチェックは適切か
  • 機密情報がハードコードされていないか
  • 入力値のサニタイズは行われているか

例:SQLインジェクション

typescript
// レビュー対象のコード
async function findUser(name: string) {
  const query = `SELECT * FROM users WHERE name = '${name}'`;
  return await db.execute(query);
}

// レビューコメント:
// 🚨 [Must Fix] SQLインジェクションの脆弱性があります
// ユーザー入力を直接クエリに結合しています。
// name に "'; DROP TABLE users; --" が渡されると
// テーブルが削除されます。

// 改善案: パラメータ化クエリを使用
async function findUser(name: string) {
  const query = 'SELECT * FROM users WHERE name = $1';
  return await db.execute(query, [name]);
}

// さらに良い: ORMを使用
async function findUser(name: string) {
  return await userRepository.findOne({ where: { name } });
}

5. パフォーマンス(Performance)

パフォーマンスに問題がないか確認します。

チェック項目

  • N+1クエリ問題はないか
  • 不要なループや計算はないか
  • 大量データの処理は考慮されているか
  • 適切なインデックスが利用されているか

例:N+1クエリ問題

typescript
// レビュー対象のコード
async function getOrdersWithProducts() {
  const orders = await orderRepository.find();           // 1回のクエリ
  for (const order of orders) {
    order.products = await productRepository.findByOrderId(order.id);  // N回のクエリ
  }
  return orders;
}
// → 注文が100件あれば、101回のDBクエリが発生する

// レビューコメント:
// ⚠️ [Should Fix] N+1クエリ問題が発生しています。
// 注文数に比例してクエリ数が増加し、パフォーマンスが劣化します。

// 改善案: JOINまたはEager Loading を使用
async function getOrdersWithProducts() {
  return await orderRepository.find({
    relations: ['products'],  // Eager Loading で1回のクエリに
  });
}

6. テスト(Testing)

テストが十分に書かれているか確認します。

チェック項目

  • 新しいコードに対応するテストがあるか
  • 正常系・異常系の両方がテストされているか
  • テストが独立して実行できるか
  • テスト名が意図を表しているか

例:テスト不足

typescript
// レビュー対象のテスト
describe('calculateDiscount', () => {
  it('should calculate discount', () => {
    expect(calculateDiscount(100, 0.1)).toBe(90);
  });
});

// レビューコメント:
// ⚠️ [Should Fix] テストケースが不足しています。
// 以下の追加をお願いします:
// - 割引率 0% のケース
// - 割引率 100% のケース
// - 境界値(price = 0)
// - 異常値(負の価格、範囲外の割引率)

// 改善案
describe('calculateDiscount', () => {
  it('should apply 10% discount', () => {
    expect(calculateDiscount(100, 0.1)).toBe(90);
  });

  it('should return original price with 0% discount', () => {
    expect(calculateDiscount(100, 0)).toBe(100);
  });

  it('should return 0 with 100% discount', () => {
    expect(calculateDiscount(100, 1)).toBe(0);
  });

  it('should handle zero price', () => {
    expect(calculateDiscount(0, 0.5)).toBe(0);
  });

  it('should throw for negative price', () => {
    expect(() => calculateDiscount(-100, 0.1)).toThrow();
  });

  it('should throw for invalid discount rate', () => {
    expect(() => calculateDiscount(100, 1.5)).toThrow();
    expect(() => calculateDiscount(100, -0.1)).toThrow();
  });
});

7. ドキュメント(Documentation)

必要なドキュメントが更新されているか確認します。

チェック項目

  • 公開APIのドキュメントは更新されているか
  • 複雑なロジックにコメントがあるか
  • READMEの更新が必要ではないか
  • 変更履歴(CHANGELOG)は更新されているか

レビューの進め方

推奨の確認順序

1. PRの説明文を読む(なぜこの変更か理解する)
         ↓
2. 変更ファイルの一覧を確認(全体像を把握する)
         ↓
3. テストを最初に読む(仕様を理解する)
         ↓
4. 実装コードを確認(正確性・設計・可読性)
         ↓
5. セキュリティ・パフォーマンスを確認
         ↓
6. ドキュメントの更新を確認

まとめ

観点重要度主なチェック項目
正確性最高バグ、エッジケース、エラーハンドリング
設計SRP、抽象化、一貫性
可読性命名、ネスト、マジックナンバー
セキュリティ最高SQLi、XSS、認証認可、機密情報
パフォーマンスN+1、不要なループ、インデックス
テストカバレッジ、正常系・異常系
ドキュメントAPI docs、コメント、README

チェックリスト

  • レビューの7つの観点を列挙できる
  • 正確性の観点でエッジケースを指摘できる
  • 設計の観点でSRP違反を検出できる
  • セキュリティの観点でSQLインジェクションを検出できる
  • テストの観点でテスト不足を指摘できる
  • レビューの推奨確認順序を説明できる

次のステップへ

チェックリストを手に入れたら、次はレビューコメントの書き方を学びます。相手に伝わる、建設的なフィードバックの方法を見ていきましょう。


推定読了時間: 25分