ConstructiCat Logo
CodeBust.
Browse section ▾

Защитный перебор.

ИИ-ассистенты подстраховываются от сбоев, которые не могут произойти, оборачивая уже безопасный код в избыточные проверки на null, мёртвые охранные условия и всеохватные блоки try/except, добавляющие сложность без добавления безопасности.

##Signs and Symptoms

Ревьюер замечает защитный перебор, когда функция тратит больше строк на защиту от невозможных состояний, чем на собственно работу. Характерные признаки:

  • Охранные условия для случаев, которые система типов уже гарантирует — проверки на null/undefined у non-nullable типизированного параметра, typeof x === "string" для string.
  • Дублирующиеся или поглощённые охранные условияif (!user) return, за которым тут же следует if (user === null || user === undefined) return.
  • Всеохватный try/catch вокруг кода, который не может бросить исключение, часто проглатывающий ошибку или просто перебрасывающий её ("на всякий случай").
  • Логика для "фантомных" краевых случаев — ветки, обрабатывающие ввод, который ни один вызывающий не может породить. OX Security обнаружила, что ИИ регулярно добавляет "логику для воображаемых краевых случаев".
  • Защитные блоки скопированы, а не извлечены, поэтому одно и то же охранное условие появляется в пяти местах.
// 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";
  }
}

Девять строк подстраховки оборачивают одну строку намерения. Противоречие, отмеченное ревьюерами: та же модель часто опускает единственную важную проверку (например, валидацию недоверенного внешнего ввода), при этом чрезмерно защищая внутренние вызовы — защитность раскидана по наитию, а не по модели угроз.

##Reasons for the Problem

Почему модели это порождают

  • Избегание риска на уровне следующего токена / "услужливость" RLHF. Модели настроены выглядеть основательными и избегать "ошибочности". Выдать лишний охранный блок или try/catch малорискованно для цели следующего токена и читается как добросовестность, поэтому это пересэмплируется. Ревьюер, работающий с большими объёмами, отметил, что повторяющиеся цепочки if (array && array.length > 0) — "признак того, что модель не вполне уверена в потоке кода" — охрана служит подстраховкой против собственной неуверенности модели.
  • Нет контекста потока данных всего репозитория. Модель не видит, что вызывающий код уже провалидировал аргумент или что тип TypeScript делает ветку недостижимой. arXiv 2509.20491 (Специфичные для ИИ запахи кода) показывает, что LLM "испытывают трудности с запахами кода, зависящими от потока или чувствительными к значениям" — когда они не могут рассуждать о потоке, они по умолчанию защищают всё локально.
  • Карго-культ безопасных промптов. Исследования промптинга безопасного кода обнаружили, что на просьбу "сделай это безопасным" LLM "добавляют блоки try-catch как самостоятельную меру безопасности без других улучшений безопасности... обычно когда не могут выявить конкретные уязвимости". Защитность подменяет понимание.
  • Смещение обучающих данных к многословному, учебному коду, демонстрирующему каждую проверку ради педагогики, плюс отказ от рефакторинга: OX Security обнаружила избегание рефакторингов в 80–90% ИИ-кода и сверхспецификацию в 80–90% — модель добавляет, удаляет редко.

Почему это вредит

  • Сопровождаемость и нагрузка на ревью. Таксономия неэффективностей LLM на Python (arXiv 2503.06327) каталогизирует избыточную валидацию ввода, чрезмерно защитную обработку ошибок и ненужные охранные условия, заключая, что эти "защитные паттерны добавляют сложность без пропорциональной выгоды для безопасности". Ревьюерам приходится прочитывать каждую мёртвую ветку, чтобы убедиться, что она действительно мертва.
  • Корректность, а не только захламление. Всеохватные блоки, проглатывающие или обобщённо обрабатывающие ошибки, прячут настоящие сбои; try/catch, возвращающий значение по умолчанию, превращает ошибку в молчаливое неверное поведение. Охранные условия также создают мёртвые ветки, которые тесты исправно покрывают, раздувая покрытие бессмысленными тестами (ещё одна находка OX).
  • Накопление техдолга. Анализ GitClear 2025 года обнаружил, что доля рефакторинга в изменённых строках упала с 25% (2021) до менее 10% (2024), тогда как скопированные строки выросли до 12,3%, а дублированные блоки выросли примерно в 8 раз. Защитный шаблонный код — это ровно тот код, который клонируется вместо извлечения, а клонированные блоки коррелируют с на 15–50% большим числом дефектов.
  • Более высокая когнитивная сложность на функцию затрудняет поиск настоящей логики, замедляя каждое будущее изменение.

##Treatment

Тактики ревью и промптинга

  • Сделайте контракт явным, чтобы охранные условия стали доказуемо ненужными. Скажите модели: "user non-nullable и уже провалидирован вызывающим — не перепроверяй его". Опирайтесь на типы: при включённом strictNullChecks правило @typescript-eslint/no-unnecessary-condition само отметит мёртвые охранные условия.
  • Просите соразмерную защиту, а не сплошную. Спрашивайте: "Обрабатывай только те ошибки, для которых можешь описать конкретный триггер. Валидируй недоверенный/внешний ввод на границе; доверяй внутренним вызовам". Это отделяет настоящую валидацию ввода (оставьте) от фантомных краевых случаев (удалите).
  • Требуйте, чтобы модель запускала линтер/проверку типов и удаляла то, что они отмечают, до возврата кода — замкните петлю так, как рекомендуют практики работы с агентами (линтеры, проверки типов, тесты как автоматические сигналы обратной связи).
  • Просите её удалять, а не только добавлять: "Отрефактори до минимума кода, удовлетворяющего спецификации; удали недостижимые ветки и блоки catch, которые не могут сработать". Это противодействует задокументированному смещению "избегания рефакторингов".

Рефакторинг

Назовите классические приёмы: Remove Dead Code (Удаление мёртвого кода), Consolidate Conditional Expression (Консолидация условного выражения), Replace Nested Conditional with Guard Clauses (Замена вложенных условий охранными выражениями) и (для ввода, который действительно нуждается в проверке) Introduce Assertion / валидируйте один раз на границе вместо повторов. Там, где одно и то же охранное условие было скопировано, — Extract Function (Извлечение функции).

// ПОСЛЕ — тип гарантирует non-null; валидируем один раз, без фантомного catch
function getDisplayName(user: User): string {
  const name = user.profile.name.trim();
  return name || "Unknown";
}

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

// граница: разбираем/валидируем недоверенный ввод единожды
const user = UserSchema.parse(rawInput); // бросает исключение на плохих данных, здесь, намеренно
// ...всё ниже по потоку получает провалидированный `User` и не нуждается в перепроверке

Эмпирическое правило для ревьюеров: каждое охранное условие и каждый catch должны отвечать на вопрос "какой конкретный вызывающий или ввод это запускает?" Если ответ — "ничего — на всякий случай", удалите его.

##Detected by