Промпты для ревью кода: 16 готовых проверок

Шестнадцать кусков кода, в каждом спрятан настоящий дефект и лежит вопрос, который задал бы ревьюер. Карточка сразу выставляет язык, фокус и среду, где код работает: замените пример своим и нажмите «Применить».

Баги и логика

Найти ошибку в коде, который собирается и всё равно врёт: изменяемый аргумент по умолчанию в Python, ноль, принятый за пустоту, сравнение времени без зоны и регрессия, приехавшая с пул-реквестом.

Изменяемый аргумент по умолчанию

Классическая ловушка Python: список из сигнатуры живёт между вызовами и копит чужие данные.

Промт

Проверь функцию корзины: покупатели жалуются, что находят в заказе товары, которых не добавляли.def add_item(item, cart=[]): cart.append(item) return cart

Результат

Строка 1 · критично · багcart=[] создаётся один раз, при импорте модуля, а не на каждый вызов: второй покупатель продолжает наполнять корзину первого — и так до перезапуска процесса.Правка: cart: list | None = None, а первой строкой тела — if cart is None: cart = [].

Ноль, принятый за пустоту

Проверка на истинность прячет законный ноль, и клиент с пустым балансом видит заглушку.

Промт

Посмотри на эту проверку: правда ли она отличает отсутствие значения от нуля?def render_balance(user): balance = user.get("balance") if not balance: return "нет данных" return format_money(balance)

Результат

Строка 3 · существенно · багif not balance одинаково ловит None, 0 и 0.0: тот, кто потратил всё до копейки, видит «нет данных» вместо честного нуля.Правка: if balance is None — пустоту проверяем явно, а ноль уходит в формат как обычное число.

Сравнение времени без зоны

datetime.now() отдаёт локальное время сервера, а срок жизни токена хранится в UTC.

Промт

Проверь расчёт срока жизни токена. Сервис работает на серверах в разных часовых поясах.def is_expired(token): return datetime.now() > token.expires_at

Результат

Строка 2 · критично · багdatetime.now() — наивное локальное время, expires_at приходит из базы в UTC: на московском сервере токены живут на три часа дольше, а против aware-значения сравнение сразу падает с TypeError.Правка: datetime.now(timezone.utc) и одно правило на весь проект — в базе только aware-время в UTC.

Регрессия в пул-реквесте

Ревью не файла, а самого изменения: что уезжает в прод вместе с этим коммитом.

Промт

Отревьюй этот diff из пул-реквеста: что именно уходит в прод вместе с ним?@@ -12,8 +12,7 @@ def apply_discount(order, percent): if order.paid: raise ValueError("order already paid")- if percent > 90:- raise ValueError("discount too large") order.total = order.total * (100 - percent) / 100+ order.discount_percent = percent

Результат

Строки 15-16 · критично · багУдалённая проверка была единственным ограничением на percent: при 150 сумма заказа уходит в минус, и касса превращает её в возврат.Правка: вернуть проверку или перенести её в валидацию запроса, а в тесты добавить случай percent=150.

Безопасность

Проверка кода на уязвимости начинается там, где чужой ввод доходит до базы и до диска: SQL-инъекция в строке поиска, секретный токен, попавший в репозиторий со скриптом, и загрузка файла мимо своей папки.

SQL-инъекция в поиске

Адрес из формы склеивается со строкой SQL — дальше базой распоряжается тот, кто заполнил форму.

Промт

Проверь эту функцию поиска пользователя. Email приходит прямо из формы на сайте.def find_user(conn, email): query = "SELECT * FROM users WHERE email = '" + email + "'" return conn.execute(query).fetchone()

Результат

Строка 2 · критично · безопасностьemail попадает в текст SQL склейкой: значение с кавычкой закрывает условие, и всё, что идёт дальше, база выполняет как ваш собственный запрос — DROP TABLE включительно.Правка: параметр вместо склейки — conn.execute("SELECT id, email FROM users WHERE email = %s", [email]); заодно уходит SELECT *, который сейчас тянет и хеш пароля.

Токен внутри скрипта

Боевой ключ лежит в репозитории и попутно печатается в лог сборки.

Промт

Проверь этот скрипт деплоя: есть ли здесь что-то опасное с точки зрения безопасности?#!/usr/bin/env bashAPI_TOKEN="sk_live_9f3c1ad84b22"echo "deploying with $API_TOKEN"curl -H "Authorization: Bearer $API_TOKEN" -X POST https://api.example.com/deploy

Результат

Строки 2-3 · критично · безопасностьБоевой токен записан в файл, который лежит в git: он есть в истории у каждого, кто когда-либо клонировал репозиторий, а echo дублирует его в лог сборки, который читают куда шире.Правка: брать из окружения ($API_TOKEN без значения по умолчанию), убрать echo, а сам ключ считать утёкшим и отозвать.

Выход за пределы папки загрузок

Имя файла берётся из запроса как есть, и пара «точка-точка-слеш» уводит к любому файлу сервера.

Промт

Проверь этот эндпоинт отдачи файла: имя файла приходит из строки запроса.def download(request): name = request.args.get("file") path = os.path.join("/var/app/uploads", name) return send_file(path)

Результат

Строка 3 · критично · безопасностьos.path.join спокойно выходит из /var/app/uploads, когда имя равно ../../etc/passwd, а абсолютный путь и вовсе отбрасывает первый аргумент: скачать можно любой файл, доступный процессу.Правка: os.path.basename(name), затем сверить os.path.realpath результата с папкой загрузок и отдавать файл только при совпадении.

Производительность

Оптимизация производительности начинается с очевидного: проблема N+1, когда запросы уходят в цикл, выборка, которая не использует индекс и читает всю таблицу, и лог, целиком приезжающий в память.

Запросы в цикле

Отчёт ходит в базу дважды на каждого пользователя: тысяча строк превращается в две тысячи запросов.

Промт

Посмотри на этот отчёт: на тысяче пользователей он отвечает минуту. Куда уходит время?def orders_report(user_ids): rows = [] for user_id in user_ids: user = db.query("SELECT name FROM users WHERE id = %s", user_id) orders = db.query("SELECT total FROM orders WHERE user_id = %s", user_id) rows.append((user.name, sum(o.total for o in orders))) return rows

Результат

Строки 3-5 · существенно · производительностьДва запроса на пользователя: тысяча пользователей — две тысячи обращений по сети, и время съедают не расчёты, а round-trip.Правка: один запрос с JOIN и GROUP BY users.id либо два запроса с IN и склейка в памяти.

Запрос мимо индекса

Функция на колонке и процент в начале LIKE выключают индексы: база читает таблицу целиком.

Промт

Проверь этот запрос: на таблице в десять миллионов строк он идёт двадцать секунд.SELECT *FROM ordersWHERE date_trunc('day', created_at) = '2026-09-01' AND lower(email) LIKE '%@example.com'ORDER BY created_at DESC

Результат

Строки 3-4 · существенно · производительностьdate_trunc на created_at делает индекс бесполезным, а LIKE с процентом в начале не может воспользоваться им в принципе: остаётся seq scan по всей таблице, и SELECT * тянет ещё и колонки, которые никто не читает.Правка: сравнивать created_at с диапазоном (от полуночи 1 сентября и меньше полуночи 2-го), домен индексировать отдельно или хранить в своей колонке, а в SELECT оставить только нужные поля.

Лог целиком в памяти

Файл читается одним куском, поэтому размер лога становится размером процесса.

Промт

Проверь эту функцию подсчёта ошибок в логе. Файлы бывают по несколько гигабайт.def count_errors(path): lines = open(path).read().split("\n") return len([line for line in lines if "ERROR" in line])

Результат

Строка 2 · существенно · производительностьread() поднимает в память весь файл, split удваивает расход, а список внутри len() держит третью копию: на восьмигигабайтном логе процесс первым встретит OOM-killer. Файл при этом ни разу не закрывается.Правка: with open(path) as f и sum(1 for line in f if "ERROR" in line) — построчно и с постоянным расходом памяти.

Читаемость и стандарты

Мелочи, из которых складывается чистый код: четыре вложенных условия стоит упростить до ранних возвратов, имена из одной буквы спорят с кодстайлом, а ставка НДС скопирована сразу в две функции.

Четыре вложенных условия

Правило отправки письма спрятано на пятом уровне отступа и читается только целиком.

Промт

Оцени читаемость этой функции: её приходится дочитывать до конца, чтобы понять условие.def notify(user): if user is not None: if user.email is not None: if user.subscribed: if not user.banned: send_email(user.email) return True return False

Результат

Строки 2-5 · незначительно · читаемостьЧетыре вложенных if — это одно правило, разложенное лестницей: чтобы понять, кому уйдёт письмо, нужно удержать в голове все четыре условия сразу, а PEP 8 просит плоскую форму.Правка: ранние выходы — if user is None: return False и так далее, — тогда тело остаётся на одном уровне отступа.

Имена против кодстайла

Метод с заглавной буквы и переменные из одной буквы: ругается и RuboCop, и следующий читатель.

Промт

Проверь этот метод по RuboCop: что здесь противоречит привычному стилю Ruby?def CalcTotal(o) t = 0 o.each do |i| t = t + i.price * i.qty end tend

Результат

Строки 1-4 · незначительно · стандартыNaming/MethodName: имена методов в Ruby пишутся в snake_case, а CamelCase здесь читается как константа. Имена o, t и i ничего не говорят о содержимом, а накопление суммы вручную — ровно работа sum.Правка: def calc_total(items) и items.sum do |item| item.price * item.qty end — четыре строки превращаются в одну.

Ставка НДС в двух местах

Одно правило скопировано в две функции и уже разошлось: в счёте 20 процентов, в чеке 19.

Промт

Проверь эти две функции: они считают одно и то же?def invoice_total(order): return round(order.subtotal * 1.2, 2)def receipt_total(order): return round(order.subtotal * 1.19, 2)

Результат

Строки 2 и 5 · существенно · архитектураОдно бизнес-правило записано дважды и уже разъехалось: счёт считает по 20 процентам, чек — по 19, и клиент видит две разные суммы за один заказ.Правка: одна константа VAT_RATE и одна функция, которой пользуются обе; тогда ставка меняется ровно в одном месте.

Тесты и надёжность

Всё держится, пока ничего не падает: покрытие тестами закрывает только счастливый путь, исключение молча проглочено в блоке try, а бэкап уезжает без единой проверки результата.

Тест только на счастливый путь

Единственный зелёный случай создаёт ощущение покрытия, которого нет.

Промт

Оцени эти тесты для apply_discount: чего в них не хватает?def test_apply_discount(): order = Order(total=100) apply_discount(order, 10) assert order.total == 90

Результат

Строка 1 · существенно · тестыПокрыт один случай — обычная скидка на неоплаченный заказ. Ни ноля, ни ста процентов, ни отрицательного значения, ни уже оплаченного заказа, ни округления на сумме 33,33: любая из этих веток может сломаться незаметно.Правка: parametrize на граничные значения и отдельный тест с pytest.raises на percent=150 и на оплаченный заказ.

Проглоченное исключение

except Exception: pass превращает падение в «ok» и стирает след из логов.

Промт

Проверь это сохранение профиля: пользователи говорят, что изменения иногда пропадают.def save_profile(user, data): try: db.update(user.id, data) search.reindex(user.id) except Exception: pass return "ok"

Результат

Строки 5-6 · критично · багexcept Exception: pass проглатывает всё подряд и возвращает "ok", даже когда запись не прошла: пользователь видит успех, данных нет, в логах пусто. Две операции под одним try вдобавок делают падение переиндексации неотличимым от падения базы.Правка: ловить конкретные исключения, писать logger.exception и возвращать честный статус; переиндексацию вынести отдельно, чтобы её сбой не отменял сохранение.

Бэкап без проверки результата

Скрипт не смотрит на коды возврата и удаляет старые копии раньше, чем выяснится, что новая пустая.

Промт

Проверь этот скрипт ночного бэкапа: восстановиться из последней копии не получилось.#!/usr/bin/env bashpg_dump "$DATABASE_URL" > /backup/db.sqlgzip -f /backup/db.sqlfind /backup -name "db.sql.gz" -mtime +7 -delete

Результат

Строки 2-4 · критично · багНет set -euo pipefail: если pg_dump упал, файл всё равно создан — пустой, gzip спокойно его сжимает, а find удаляет рабочие копии старше недели; через семь дней не остаётся ни одного целого бэкапа. Незаданный $DATABASE_URL проходит так же незаметно.Правка: set -euo pipefail первой строкой, проверка размера дампа после pg_dump и удаление старых копий только после успешной новой.

Промпты для ИИ-ревью кода: 16 готовых проверок | iBro