26.11.2025

Conventional Comments: как перестать бомбить друг на друга в ревью

Это про то, как превратить code review из иди нахуй лесом со своим кодом в братан, давай вместе сделаем это огонь.

Два мира и две вселенные

Вася (как выглядет обычное ревью)

Pull Request: Добавил форму регистрации

 Это плохая реализация
 Переделай по..
 Не так сделалл
 Почему ты не использовал паттерн НОУНЕЙМ?

Что думает Вася 😰

  • ЧТО БЛЯТЬ ПЛОХО?!
  • КАК переделать?
  • Это вообще обязательно?
  • Я тут 3 дня пахал, а мне "плохо"…
Ревью
Васина рекация вполне оправдана

Результат: Вася идет курить, думает о смене работы, PR висит неделю.

Петя (Conventional Comments)

Pull Request: Добавил форму регистрации

 praise: Огонь валидация! Учел все edge cases 🔥
 suggestion (non-blocking): Можно вынести валидацию в отдельный класс. Не критично, можно в следующем PR.
 issue (blocking): Тут SQL-инъекция в поле "имя". Надо пофиксить перед мержем.
 question: Почему bcrypt, а не argon2? Просто интересно.
 nitpick: Опечатка "валидатция"  "валидация"

Что думает Петя:

  • Кайф, похвалили! 😊
  • Рефакторинг сделать потом, понял
  • Упс, SQL-инъекция, исправлю за 5 минут
  • Argon2, хм, погуглю, интересно
  • Опечатку за секунду поправлю

Результат: Исправил за полчаса, смержил, пошел пить пиво.

Разница
Подума об этом

Суть в двух словах

Каждый комментарий начинается с ярлыка, который сразу говорит:

🔴 Красный (blocking): Стоп! Мержить нельзя
🟡 Желтый (non-blocking): Можно мержить, но лучше исправить
🟢 Зеленый (praise): Красава, так держать!

Формат

<ярлык> [важность]: что не так

[опционально: как исправить]

Пример:

suggestion (non-blocking): Можно короче через array_map

$result = array_map(fn($item) => $item * 2, $array);

Но если привык к foreach, оставь как есть.

Ярлыки для жизни

praise (похвала)

Когда: Когда хочешь сказать "красава"

praise: О, хорошее название класса. Сразу все понятно.

praise: Крутой рефакторинг! Код стал в 2 раза короче.

Правило: На каждые 3 критических комментария, нужна минимум 1 похвала. Иначе ты токсик.

issue (проблема)

Когда: Нашел баг или косяк

issue (blocking): SQL-инъекция, братан

Используй prepared statements:
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);

issue (blocking): Деление на ноль, если $count === 0

Добавь проверку перед делением.

Issue почти всегда blocking. Баги надо чинить, а не откладывать.

suggestion (предложение)

Когда: Видишь, как можно лучше, но текущий вариант тоже ок

suggestion (non-blocking): Можно вынести в отдельный метод

Будет проще тестировать. Но если времени нет, забей.

suggestion: Добавь кеширование для этого запроса

Он дергается на каждой странице, а данные меняются раз в час.
Redis решит проблему.

question (вопрос)

Когда: Что-то непонятно или хочешь узнать причину

question: Почему setTimeout вместо setInterval?

Просто хочу понять логику.

question: Зачем тут try-catch, если ошибка все равно летит дальше?

try {
    $result = $this->api->call();
} catch (Exception $e) {
    throw $e; // <- зачем?
}

Важно: Вопросы - не наезд, а способ понять и научиться.

nitpick (придирка)

Когда: Мелочи, которые не влияют на работу, но написать хочется

nitpick: Лишний пробел в конце строки

nitpick: Можно переименовать $data в $userData

nitpick: Опечатка в комментарии

Nitpick всегда non-blocking. Это не повод задерживать мерж.

🔥 Жизненные примеры

Пример 1: Большой метод

public function processOrder($orderId) {
    $order = Order::find($orderId);

    if ($order->status !== 'pending') {
        throw new Exception('Order not pending');
    }

    $user = User::find($order->user_id);

    if ($user->balance < $order->total) {
        throw new Exception('Insufficient balance');
    }

    $user->balance -= $order->total;
    $user->save();

    $order->status = 'paid';
    $order->save();

    Mail::to($user->email)->send(new OrderPaid($order));

    foreach ($order->items as $item) {
        $product = Product::find($item->product_id);
        $product->stock -= $item->quantity;
        $product->save();
    }

    return $order;
}

Review:

praise: Хорошо обработал все шаги оплаты.

---

issue (blocking): Нет транзакции — если упадет на середине, 
деньги спишутся, а заказ не обновится

DB::transaction(function () use ($orderId) {
    // вся логика здесь
});

---

suggestion (non-blocking): Метод делает слишком много

Можно разбить на validateOrder(), chargeUser(), updateStock().
Проще тестировать и переиспользовать.

Но можно в следующем PR, если есть время.

---

question: Почему письмо внутри транзакции?

Если письмо долго отправляется, транзакция висит.
Обычно такое через очередь делают.

---

nitpick: N+1 запросов в цикле

$order->load('items.product') решит проблему.

Пример 2: Загрузка аватарки

async upload(req, res) {
  const file = req.file;

  if (!file) {
    return res.status(400).json({ error: 'No file' });
  }

  const allowedTypes = ['image/jpeg', 'image/png'];
  if (!allowedTypes.includes(file.mimetype)) {
    return res.status(400).json({ error: 'Invalid type' });
  }

  if (file.size > 5 * 1024 * 1024) {
    return res.status(400).json({ error: 'File too large' });
  }

  const filename = `${Date.now()}-${file.originalname}`;
  const path = `uploads/avatars/${filename}`;

  await fs.writeFile(path, file.buffer);

  await User.update(
    { avatar: filename },
    { where: { id: req.user.id } }
  );

  res.json({ avatar: filename });
}

Review:

praise: Отличная валидация типов и размера! 👏

---

issue (blocking): Path Traversal уязвимость

Если пользователь загрузит "../../etc/passwd", 
он может перезаписать системные файлы.

Используй UUID вместо оригинального имени:

const { v4: uuid } = require('uuid');
const ext = path.extname(file.originalname);
const filename = `${uuid()}${ext}`;

---

suggestion (blocking): Нужно удалять старый аватар

Сейчас старые файлы остаются и занимают место.

const user = await User.findByPk(req.user.id);
if (user.avatar) {
  await fs.unlink(`uploads/avatars/${user.avatar}`);
}

---

suggestion (non-blocking): Можно добавить resize до 200x200

Библиотека sharp:

await sharp(file.buffer)
  .resize(200, 200)
  .webp({ quality: 80 })
  .toFile(path);

Но это можно в следующем PR.

---

nitpick: Магическое число 5 * 1024 * 1024

const MAX_AVATAR_SIZE = 5 * 1024 * 1024; // 5 MB

Как внедрять это?

Шаг 1: Покажи команде

  1. Собери команду на пятничной пьянке (или в Zoom, если удаленка)
  2. Покажи им статью
  3. Разбери примеры в вашем ревью, плохой и хороший комментарий
  4. Спроси: Что думаете?

Шаг 2: Договоритесь о ярлыках

Минимальный набор:

  • praise - похвала
  • issue - баг (обычно blocking)
  • suggestion - предложение (обычно non-blocking)
  • question - вопрос
  • nitpick - мелочь (всегда non-blocking)

Правила:

  1. На каждые 3 критических комментария, минимум 1 похвала
  2. blocking только для критичных вещей
  3. Если сомневаешься, ставь non-blocking
  4. Будь человеком, а не мудаком

Шаг 3: Создайте шпаргалку

Запости в Slack/Telegram:

🎉 praise: Красава!
🐛 issue (blocking): Баг, нельзя мержить
💡 suggestion (non-blocking): Можно лучше, но не обязательно
❓ question: Вопрос
🔍 nitpick: Мелочь

Шаг 4: Попробуйте на 2-3 PR

Не заставляйте всех сразу. Пусть добровольцы попробуют.

Если ок, раскатывайте на всех.

Шаг 5: Автоматизация (опционально)

VS Code snippet:

{
  "CC Suggestion": {
    "prefix": "ccs",
    "body": "suggestion (non-blocking): $1\n\n$2"
  },
  "CC Issue": {
    "prefix": "cci",
    "body": "issue (blocking): $1\n\n$2"
  },
  "CC Praise": {
    "prefix": "ccp",
    "body": "praise: $1"
  }
}

Теперь пишешь ccs + Tab и готово.

Расширение для GitHub:

Conventional Comments Button — добавляет кнопки для быстрой вставки ярлыков.

Продвинутые фишки для зануд

Правило Сэндвича

  1. Похвала, что хорошо
  2. Критика, что можно лучше
  3. Поддержка, как помочь

Пример:

praise: Отлично обработал edge cases!
suggestion (non-blocking): Можно добавить кеширование.
Если нужна помощь с Redis, пинганй. Или можем в следующем PR.

Правило Объясни почему

Плохо:

issue (blocking): Не используй var

Хорошо:

issue (blocking): Не используй var, используй const или let

var имеет function scope, что может привести к багам:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Выведет: 3, 3, 3 (ожидали: 0, 1, 2)

Правило Покажи пример

Плохо:

suggestion: Можно упростить

Хорошо:

suggestion (non-blocking): Можно короче через деструктуризацию

Было:
const name = user.name;
const email = user.email;

Может быть:
const { name, email } = user;

Частые косяки

Косяк 1: Все blocking

Проблема: Ревьюер ставит blocking на каждую мелочь.
Решение: blocking только для багов, безопасности, архитектуры. Все остальное non-blocking.

Косяк 2: Нет похвалы

Проблема: Все комментарии, только критика. Автор в депрессии.
Решение: Минимум 1 praise на каждые 3 критических комментария. Всегда можно найти хорошее.

Косяк 3: Комментарии без объяснения

Проблема:

issue (blocking): Не так как надо

Решение: Всегда объясняй ЧТО не так, ПОЧЕМУ это проблема, КАК исправить.

Косяк 4: Формализм ради формализма

Проблема: Команда тратит больше времени на правильное оформление, чем на ревью.
Решение: Conventional Comments = инструмент, а не цель. Главное понятность и дружелюбность.

И главное

  • Цель ревью = сделать код лучше, а не показать, какой ты умный
  • Автор PR = твой коллега, а не враг
  • Хороший комментарий учит, а не унижает
  • Похвала работает лучше критики

И да, теперь когда кто-то напишет тебе плохо, ты можешь скинуть ему эту статью 😄

P.S. Если после этой статьи ты все еще пишешь переделай без объяснений - иди нахуй