Evitação de Refatoração.
Assistentes de IA raramente reestruturam o código existente: eles penduram novos ramos, parâmetros de flag e camadas de wrapper sobre o que já existe, então funções e condicionais não param de crescer enquanto a refatoração — o retrabalho que mantém uma base de código saudável — desaparece em silêncio.
##Sinais e sintomas
O revisor vê um diff composto quase inteiramente de adições a código que já existe. O modelo resolveu o prompt empilhando mais um ramo, uma flag ou uma camada sobre a função indicada, em vez de remodelar essa função para que a mudança se encaixe. Sinais reveladores:
- Diffs só de adições: nenhuma linha movida, renomeada ou apagada; a função tocada só fica mais longa.
- Parâmetros de flag:
isWholesale,skipValidation,legacy = falseacrescentados a uma assinatura existente, cada um mudando o comportamento lá no fundo do corpo. - Proliferação de ramos: cada novo requisito vira mais um
else ifoucasena mesma função em vez de uma tabela de consulta, uma estratégia ou uma chamada polimórfica. - Um wrapper por cima:
processOrderV2chamaprocessOrdere remenda o resultado em vez de alterá-lo; o caminho antigo e o novo convivem lado a lado. - Métricas subindo no mesmo lugar: complexidade cognitiva, tamanho e número de parâmetros das mesmas funções sobem commit após commit — um hotspot que cresce.
- Flags mortas nunca removidas: parâmetros que todos os chamadores agora passam do mesmo jeito, mantidos "por via das dúvidas".
// Cada pedido acrescentou um parâmetro e um ramo; nada jamais foi reestruturado.
function priceOrder(
items: Item[],
discount?: number,
isWholesale = false,
applyVat = true,
couponCode?: string,
) {
let total = items.reduce((s, i) => s + i.price * i.qty, 0);
if (discount) total = total * (1 - discount);
if (isWholesale) {
if (total > 1000) total = total * 0.9;
else if (total > 500) total = total * 0.95;
}
if (couponCode === "SPRING") total = total - 10;
else if (couponCode === "VIP") total = total * 0.85;
if (applyVat) total = total * 1.2;
return total;
}
Cada pedido acrescentou um parâmetro e um ramo, e cada diff mexeu só no final da função. Nada está duplicado, então um detector de clones fica em silêncio; o smell aparece como crescimento de complexidade e de tamanho.
##Causas do problema
Por que os modelos produzem isso
- Viés aditivo da predição do próximo token. Um LLM completa o prompt que tem à frente. Acrescentar um ramo ou um parâmetro no ponto da mudança é a continuação localmente mais provável e de menor risco; remodelar a função exige manter no contexto de trabalho o módulo inteiro e seus chamadores e prever uma edição globalmente consistente, o que é mais difícil e não é o que o prompt imediato recompensa.
- Contexto escasso do repositório. Assistentes de código raramente carregam todos os chamadores e testes. Sem ver quem depende de uma função, o modelo não tem como saber se é seguro reestruturá-la, então deixa a estrutura como está e adiciona um
ifonde consegue ver. A OX Security descreve a IA como "um exército de desenvolvedores juniores talentosos, mas inexperientes" — produtivos localmente, sem olhar para a arquitetura. - Refatorar é arriscado e não é recompensado. Reestruturar mexe em código que o modelo não foi solicitado a alterar e pode quebrar chamadores e testes. Um assistente bajulador, do tipo "só faça funcionar", minimiza o raio de impacto não mexendo no código que funciona — exatamente o comportamento que a OX mediu como "evitação de refatorações" (Avoidance of Refactors) em 80–90% do código gerado por IA: a IA "se destaca gerando código novo, mas nunca volta para melhorar a arquitetura".
- Gerar é barato, apagar dá medo. Produzir 40 linhas novas não custa nada ao modelo; convencê-lo a apagar e remodelar 40 linhas existentes vai contra seu instinto de preservar.
Por que isso prejudica
- A refatoração some do histórico. A análise de 2025 da GitClear sobre 211 milhões de linhas alteradas mostrou que as linhas associadas a refatoração ("movidas") caíram de 25% das linhas alteradas em 2021 para menos de 10% em 2024, enquanto as copiadas e coladas subiram de 8,3% para 12,3% e, em 2024, superaram as movidas pela primeira vez.
- A complexidade se acumula. Cada flag pendurada dobra as combinações que a função precisa tratar e que os testes precisam cobrir; cada
else ifextra torna a próxima mudança mais arriscada — e o próximo prompt também adiciona em vez de reestruturar. - Acúmulo de dívida técnica. A refatoração é o retrabalho que mantém a entropia baixa; suprimi-la significa criar dívida que nunca é paga. A base de código cresce mais rápido do que melhora.
- Carga de revisão. Os revisores recebem diffs que só acrescentam linhas a funções que já são longas; avaliar como um novo ramo interage com a dúzia acima dele é mais difícil do que revisar uma função remodelada para absorver a mudança.
##Tratamento
Táticas de prompt / revisão
- Primeiro torne a mudança fácil. O conselho de Kent Beck — "para cada mudança desejada, torne a mudança fácil (aviso: isso pode ser difícil), depois faça a mudança fácil" — vira diretamente um prompt: "Antes de adicionar a regra do cupom, reestruture
priceOrderpara que a nova regra seja uma adição de uma linha; mantenha o comportamento idêntico e rode os testes. Depois adicione a funcionalidade." - Proíba novos parâmetros de flag. "Não adicione parâmetros booleanos ou de modo a funções existentes; se o comportamento precisar variar, proponha primeiro uma reestruturação."
- Orce a complexidade, não só a correção. Exija que o assistente rode as regras
complexity,max-paramsemax-lines-per-functiondo ESLint e a regra de complexidade cognitiva do Sonar, e informe os valores antes e depois para cada função que tocou; qualquer aumento precisa de justificativa. - Peça a refatoração como uma etapa separada. Gerar e reestruturar são tarefas diferentes: primeiro "faça funcionar", depois uma mudança separada que preserve o comportamento — "agora reestruture esta função para que os ramos virem dados ou funções separadas" —, com os testes verdes antes e depois.
- Vigie os hotspots. Acompanhe frequência de mudanças × complexidade (Code Maat ou o próprio histórico do git) dos arquivos que crescem a cada commit escrito por IA e programe a reestruturação deles antes que a próxima funcionalidade chegue ali.
A refatoração em si — Decompor Condicional e Extrair Método dão nome a cada ramo; Substituir Parâmetro por Métodos Explícitos ou Introduzir Objeto de Parâmetro substituem os parâmetros de flag; e Substituir Condicional por Polimorfismo (ou uma tabela de consulta) assume quando os ramos escolhem o comportamento por tipo ou código.
// Depois: cada regra é uma função pequena; priceOrder só as compõe.
type PriceRule = (total: number) => number;
const subtotalOf = (items: Item[]) =>
items.reduce((s, i) => s + i.price * i.qty, 0);
const VAT_RATE = 1.2;
const discount = (rate: number): PriceRule => (t) => t * (1 - rate);
const wholesale: PriceRule = (t) => (t > 1000 ? t * 0.9 : t > 500 ? t * 0.95 : t);
const COUPONS: Record<string, PriceRule> = {
SPRING: (t) => t - 10,
VIP: (t) => t * 0.85,
};
const vat: PriceRule = (t) => t * VAT_RATE;
function priceOrder(items: Item[], rules: PriceRule[]): number {
return rules.reduce((total, rule) => rule(total), subtotalOf(items));
}
// Quem chama informa quais regras se aplicam, em ordem:
// priceOrder(items, [discount(0.1), wholesale, COUPONS.SPRING, vat]);
Uma nova regra de preço agora é mais uma entrada, não mais um parâmetro e um ramo, e cada regra pode ser testada isoladamente.
Não confunda com Duplicação em Vez de Extração: lá o modelo copia lógica existente para um novo bloco quase idêntico, e os detectores de clones o encontram. Aqui nada é copiado — o modelo continua fazendo a mesma função crescer com ramos, flags e camadas, e são as métricas de complexidade e tamanho que revelam isso.
##Detectado por
- SonarQube / SonarSource S3776 — A complexidade cognitiva das funções não deve ser alta demais — sinaliza funções que continuam absorvendo novos ramos e flags
- ESLint complexity — Limite de complexidade ciclomática por função (opção max)
- ESLint max-lines-per-function — Limite de tamanho de função — pega funções que só crescem
- ESLint max-params — Limite de número de parâmetros — pega flags e opções penduradas em uma assinatura existente
- ESLint max-depth — Limite de profundidade de aninhamento — pega escadas de if/else empilhadas dentro de ramos antigos
- Code Maat entity-churn — Frequência de mudanças por arquivo a partir do histórico do git — combinada com a complexidade, revela hotspots que crescem commit após commit