Защитный перебор.
ИИ-ассистенты подстраховываются от сбоев, которые не могут произойти, оборачивая уже безопасный код в избыточные проверки на null, мёртвые охранные условия и всеохватные блоки try/except, добавляющие сложность без добавления безопасности.
##Признаки и симптомы
Ревьюер замечает защитный перебор, когда функция тратит больше строк на защиту от невозможных состояний, чем на собственно работу. Характерные признаки:
- Охранные условия для случаев, которые система типов уже гарантирует — проверки на null/undefined у non-nullable типизированного параметра,
typeof x === "string"дляstring. - Дублирующиеся или поглощённые охранные условия —
if (!user) return, за которым тут же следуетif (user === null || user === undefined) return. - Всеохватный
try/catchвокруг кода, который не может бросить исключение, часто проглатывающий ошибку или просто перебрасывающий её ("на всякий случай"). - Логика для "фантомных" краевых случаев — ветки, обрабатывающие ввод, который ни один вызывающий не может породить. Паттерн Phantom Bugs из отчёта OX Security — ИИ «переусложняет код ради маловероятных краевых случаев» — встретился в 20–30% проанализированного кода (отчёт OX).
- Защитные блоки скопированы, а не извлечены, поэтому одно и то же охранное условие появляется в пяти местах.
// User — NON-nullable тип: { profile: { name: string } }
function getDisplayName(user: User): string {
if (!user) return "Unknown"; // мёртвая: user non-nullable
if (user === null || user === undefined) return "?"; // дубликат, тоже мёртвая
try {
if (user.profile && typeof user.profile.name === "string") { // тип уже это гарантирует
const name = user.profile.name;
if (name.length > 0) {
return name.trim() !== "" ? name.trim() : "Unknown";
}
}
return "Unknown";
} catch (e) {
console.error("could not get name", e); // этот блок не может бросить исключение
return "Unknown";
}
}
Девять строк подстраховки оборачивают одну строку намерения. Противоречие, отмеченное ревьюерами: та же модель часто опускает единственную важную проверку (например, валидацию недоверенного внешнего ввода), при этом чрезмерно защищая внутренние вызовы — защитность раскидана по наитию, а не по модели угроз.
##Причины проблемы
Почему модели это порождают
- Избегание риска на уровне следующего токена / "услужливость" RLHF. Модели настроены выглядеть основательными и избегать "ошибочности". Выдать лишний охранный блок или
try/catchмалорискованно для цели следующего токена и читается как добросовестность, поэтому это пересэмплируется. Повторяющиеся цепочкиif (array && array.length > 0)выглядят как подстраховка против собственной неуверенности модели в потоке выполнения. - Нет контекста потока данных всего репозитория. Модель не видит, что вызывающий код уже провалидировал аргумент или что тип TypeScript делает ветку недостижимой. Не имея возможности проследить поток данных за пределами текущего файла, модель по умолчанию защищает всё локально.
- Карго-культ безопасных промптов. Получив просьбу «сделай это безопасным» без конкретной угрозы, модель может просто обернуть код в
try/catch, будто это само по себе мера безопасности. Защитность подменяет понимание. - Смещение обучающих данных к многословному, учебному коду, демонстрирующему каждую проверку ради педагогики, плюс отказ от рефакторинга: OX Security обнаружила избегание рефакторингов в 80–90% ИИ-кода и сверхспецификацию в 80–90% — модель добавляет, удаляет редко.
Почему это вредит
- Сопровождаемость и нагрузка на ревью. Таксономия неэффективностей в сгенерированном LLM коде на Python включает Unnecessary Conditional Block (ненужный условный блок), а также избыточные и ненужные шаги (arXiv 2503.06327) — именно этим и являются мёртвые охранные проверки. Ревьюерам приходится прочитывать каждую мёртвую ветку, чтобы убедиться, что она действительно мертва.
- Корректность, а не только захламление. Всеохватные блоки, проглатывающие или обобщённо обрабатывающие ошибки, прячут настоящие сбои;
try/catch, возвращающий значение по умолчанию, превращает ошибку в молчаливое неверное поведение. Охранные условия также создают мёртвые ветки, которые тесты исправно покрывают, раздувая покрытие бессмысленными тестами (ещё одна находка OX). - Накопление техдолга. Анализ GitClear 2025 года (211 млн изменённых строк, 2020–2024) показал, что доля строк, связанных с рефакторингом («перемещённых»), упала с 25% изменённых строк в 2021 году до менее чем 10% в 2024-м, а доля скопированных/вставленных строк за тот же период выросла с 8,3% до 12,3%; 2024 год стал первым, когда скопированных строк оказалось больше, чем перемещённых, а число блоков из пяти и более дублирующихся строк за 2024 год выросло примерно в восемь раз (GitClear 2025). Защитный шаблонный код — ровно тот код, который клонируют вместо извлечения, поэтому каждую копию приходится отдельно читать, тестировать и в итоге исправлять.
- Более высокая когнитивная сложность на функцию затрудняет поиск настоящей логики, замедляя каждое будущее изменение.
##Лечение
Тактики ревью и промптинга
- Сделайте контракт явным, чтобы охранные условия стали доказуемо ненужными. Скажите модели: "
usernon-nullable и уже провалидирован вызывающим — не перепроверяй его". Опирайтесь на типы: при включённомstrictNullChecksправило@typescript-eslint/no-unnecessary-conditionсамо отметит мёртвые охранные условия. - Просите соразмерную защиту, а не сплошную. Спрашивайте: "Обрабатывай только те ошибки, для которых можешь описать конкретный триггер. Валидируй недоверенный/внешний ввод на границе; доверяй внутренним вызовам". Это отделяет настоящую валидацию ввода (оставьте) от фантомных краевых случаев (удалите).
- Требуйте, чтобы модель запускала линтер/проверку типов и удаляла то, что они отмечают, до возврата кода — замкните петлю так, как рекомендуют практики работы с агентами (линтеры, проверки типов, тесты как автоматические сигналы обратной связи).
- Просите её удалять, а не только добавлять: "Отрефактори до минимума кода, удовлетворяющего спецификации; удали недостижимые ветки и блоки catch, которые не могут сработать". Это противодействует задокументированному смещению "избегания рефакторингов".
Рефакторинг
Назовите классические приёмы: удалите мёртвый код, примените «Объединение условных операторов», «Замена вложенных условных операторов граничным оператором» и (для ввода, который действительно нуждается в проверке) «Введение проверки утверждения» — или валидируйте один раз на границе — вместо повторных проверок. Там, где одна и та же охранная проверка была скопирована, — «Извлечение метода».
// ПОСЛЕ — тип гарантирует non-null; валидируем один раз, без фантомного catch
function getDisplayName(user: User): string {
const name = user.profile.name.trim();
return name || "Unknown";
}
Если значение действительно является недоверенным, валидируйте его один раз на краю и позвольте остальному коду доверять теперь сужённому типу:
// граница: разбираем/валидируем недоверенный ввод единожды
const user = UserSchema.parse(rawInput); // бросает исключение на плохих данных, здесь, намеренно
// ...всё ниже по потоку получает провалидированный `User` и не нуждается в перепроверке
Эмпирическое правило для ревьюеров: каждое охранное условие и каждый catch должны отвечать на вопрос "какой конкретный вызывающий или ввод это запускает?" Если ответ — "ничего — на всякий случай", удалите его.
##Обнаружение анализаторами
- typescript-eslint @typescript-eslint/no-unnecessary-condition — no-unnecessary-condition
- ESLint no-useless-catch — no-useless-catch
- SonarSource (SonarQube / eslint-plugin-sonarjs) RSPEC-2589 — Логические выражения не должны быть лишними: условия с постоянным результатом true/false (no-gratuitous-expressions)
- SonarSource (SonarQube / eslint-plugin-sonarjs) RSPEC-3776 — Когнитивная сложность (cognitive-complexity): вложенные защитные проверки увеличивают её; косвенный детектор