Logotipo do ConstructiCat
CodeBust.
Explorar a seção ▾

Excesso de Defensividade.

Assistentes de IA se protegem contra falhas que não podem acontecer, embrulhando código já seguro em verificações de null redundantes, cláusulas de guarda mortas e blocos try/except pega-tudo que adicionam complexidade sem adicionar segurança.

##Sinais e sintomas

Um revisor identifica Excesso de Defensividade quando uma função gasta mais linhas se protegendo contra estados impossíveis do que fazendo trabalho de fato. Sinais reveladores:

  • Guardas para condições que o sistema de tipos já garante — verificações de null/undefined em um parâmetro tipado não anulável, typeof x === "string" em um string.
  • Guardas duplicadas ou subsumidas — if (!user) return imediatamente seguido por if (user === null || user === undefined) return.
  • Pega-tudo try/catch em torno de código que não pode lançar, muitas vezes engolindo o erro ou apenas relançando-o ("só por precaução").
  • Lógica para casos extremos "fantasmas" — ramos que tratam entradas que nenhum chamador pode produzir. O padrão Phantom Bugs da OX Security — uma IA que "faz over-engineering para casos extremos improváveis" — apareceu em 20–30% do código analisado (relatório OX).
  • Blocos defensivos copiados e colados em vez de extraídos, de modo que a mesma guarda aparece em cinco lugares.
// User é um tipo NÃO anulável: { profile: { name: string } }
function getDisplayName(user: User): string {
  if (!user) return "Unknown";                         // morto: user é não anulável
  if (user === null || user === undefined) return "?"; // duplicado, também morto
  try {
    if (user.profile && typeof user.profile.name === "string") { // o tipo já garante isto
      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); // este bloco não pode lançar
    return "Unknown";
  }
}

Nove linhas de proteção embrulham uma linha de intenção. A contradição notada pelos revisores é que o mesmo modelo muitas vezes omite a única verificação que importa (por exemplo, validar entrada externa não confiável) enquanto protege excessivamente chamadas internas — defensividade espalhada por vibe, não por modelo de ameaças.

##Causas do problema

Por que os modelos o produzem

  • Aversão a risco do próximo token / "prestatividade" do RLHF. Os modelos são ajustados para parecer minuciosos e evitar estar "errados". Emitir uma guarda extra ou um try/catch é de baixo risco para o objetivo de próximo token e lê como consciencioso, então é superamostrado. Cadeias repetidas de if (array && array.length > 0) parecem uma proteção contra a própria incerteza do modelo sobre o fluxo do código.
  • Sem contexto de dataflow de todo o repositório. O modelo não consegue ver que um chamador já validou o argumento, ou que um tipo TypeScript torna um ramo inalcançável. Quando não consegue raciocinar sobre o fluxo de dados além do arquivo atual, recorre a proteger tudo localmente.
  • Cargo-cult de prompt de segurança. Quando solicitado a "torná-lo seguro" sem uma ameaça concreta a tratar, o modelo pode simplesmente envolver o código em try/catch, como se isso fosse por si só uma medida de segurança. A defensividade substitui a compreensão.
  • Viés dos dados de treinamento em direção a código verboso, estilo tutorial que demonstra cada verificação por pedagogia, somado a uma recusa em refatorar: a OX Security encontrou evitação de refatorações em 80–90% do código de IA e sobre-especificação em 80–90% — o modelo adiciona, ele raramente remove.

Por que isso prejudica

  • Manutenibilidade & carga de revisão. Uma taxonomia de ineficiências em código Python gerado por LLMs inclui o Unnecessary Conditional Block (bloco condicional desnecessário) e passos redundantes ou desnecessários (arXiv 2503.06327) — exatamente o que são as guardas mortas. Os revisores precisam ler cada ramo morto para confirmar que ele está morto.
  • Correção, não apenas entulho. Blocos pega-tudo que engolem ou tratam erros genericamente escondem falhas reais; um try/catch que retorna um valor padrão transforma um bug em comportamento errado silencioso. As guardas também criam ramos mortos que os testes cobrem diligentemente, inflando a cobertura com testes sem sentido (outra constatação da OX).
  • Acúmulo de dívida técnica. A análise de 2025 da GitClear, com 211 milhões de linhas alteradas (2020–2024), constatou que as linhas associadas à refatoração (linhas "movidas") caíram de 25% das linhas alteradas em 2021 para menos de 10% em 2024, enquanto as linhas copiadas/coladas subiram de 8,3% para 12,3% no mesmo período; 2024 foi o primeiro ano em que as linhas copiadas/coladas superaram as movidas, e os blocos com cinco ou mais linhas duplicadas cresceram cerca de oito vezes ao longo de 2024 (GitClear 2025). O boilerplate defensivo é exatamente o tipo de código que é clonado em vez de extraído, então cada cópia precisa ser lida, testada e, no fim, corrigida separadamente.
  • Maior complexidade cognitiva por função torna a lógica real mais difícil de encontrar, atrasando cada mudança futura.

##Tratamento

Táticas de revisão & prompting

  • Torne o contrato explícito para que as guardas se tornem comprovadamente desnecessárias. Diga ao modelo: "user é não anulável e já validado pelo chamador — não o reverifique." Apoie-se nos tipos: com strictNullChecks ativado, @typescript-eslint/no-unnecessary-condition vai sinalizar guardas mortas para você.
  • Faça prompt por defesa proporcional, não defesa generalizada. Peça: "Só trate erros para os quais você consiga descrever um gatilho concreto. Valide entrada não confiável/externa na fronteira; confie em chamadas internas." Isso separa a validação de entrada real (mantenha-a) dos casos extremos fantasmas (apague-os).
  • Exija que o modelo rode o linter/verificador de tipos e remova o que ele sinalizar antes de retornar o código — feche o loop da forma que os praticantes de agentes recomendam (linters, verificadores de tipos, testes como sinais de feedback automatizados).
  • Peça que ele apague, não apenas adicione: "Refatore para o código mínimo que satisfaz a especificação; remova ramos inalcançáveis e blocos catch que não podem disparar." Isso combate o viés documentado de "evitação de refatorações".

A refatoração

Nomeie os movimentos clássicos: apague o código morto, Consolidar Expressão Condicional, Substituir Condicional Aninhada por Cláusulas de Guarda e (para entrada que de fato precisa de verificação) Introduzir Asserção — ou valide uma vez na fronteira — em vez de repetir a verificação. Onde a mesma guarda foi copiada e colada, Extrair Método.

// DEPOIS — o tipo garante não-null; valide uma vez, sem catch fantasma
function getDisplayName(user: User): string {
  const name = user.profile.name.trim();
  return name || "Unknown";
}

Se um valor genuinamente é não confiável, valide-o uma vez na borda e deixe o resto do código confiar no tipo agora restringido:

// fronteira: faça parse/validação da entrada não confiável uma única vez
const user = UserSchema.parse(rawInput); // lança em dados ruins, aqui, de propósito
// ...tudo a jusante recebe um `User` validado e não precisa de re-proteção

Regra prática para revisores: cada guarda e cada catch precisa responder "qual chamador ou entrada concreta dispara isto?" Se a resposta for "nada — só por precaução", apague-o.

##Detectado por

  • typescript-eslint @typescript-eslint/no-unnecessary-condition — no-unnecessary-condition
  • ESLint no-useless-catch — no-useless-catch
  • SonarSource (SonarQube / eslint-plugin-sonarjs) RSPEC-2589 — Expressões booleanas não devem ser desnecessárias: condições sempre verdadeiras ou falsas (no-gratuitous-expressions)
  • SonarSource (SonarQube / eslint-plugin-sonarjs) RSPEC-3776 — Complexidade cognitiva (cognitive-complexity): verificações defensivas aninhadas a aumentam; detector indireto