Adicionei ESLint num projeto que já estava no ar e ele achou 5 bugs em produção
Meu projeto não tinha linter. Nenhum nem no backend (Fastify), nem no frontend (React + Vite). Sempre adiei porque associava linter a briga sobre aspas simples e ponto e vírgula, e isso me parecia a menor das prioridades.
Semana passada configurei. Não para padronizar estilo: liguei só as regras que apontam bug. Variável fora de escopo, hook chamado condicionalmente, catch vazio, declaração vazando entre case.
A configuração encontrou cinco bugs que já estavam em produção. Nenhum deles quebrava o build.
1. request fora do ciclo da requisição
async function deliverWithRetry(db, webhook, payload, attempt = 1) {
// ... entrega o webhook, com retry agendado por setTimeout
await db.collection('webhooks').updateOne(
{ _id: webhook._id, tenant_id: request.tenantId }, // request não existe aqui
{ $inc: statsUpdate }
);
}
Essa função roda fora do ciclo da requisição é entrega assíncrona, com retry por setTimeout. Não existe request no escopo dela. Toda atualização de estatística de webhook estourava ReferenceError.
O detalhe que dói: fui eu que introduzi, semanas antes, num PR que adicionava filtro por tenant em várias consultas. Busquei o padrão, apliquei em todas as ocorrências, e não notei que uma delas estava num escopo diferente.
2. Variável que nunca foi declarada
fastify.get('/:databaseId', { ... }, async (request, reply) => {
const { databaseId } = request.params;
const database = await db.collection('databases').findOne({
_id: objectId, // objectId não é declarado em lugar nenhum
tenant_id: request.tenantId
});
Duas rotas assim. GET e PATCH quebravam em toda chamada. Estavam ali havia meses são rotas de um recurso pouco usado, e ninguém tinha reclamado.
3, 4 e 5: componentes que estouravam ao renderizar
O frontend tinha passado por uma internacionalização recente, boa parte feita com script. Três resquícios:
- Uma tela importava
useTranslatione nunca chamava o hook. Eram 57 usos det()sem escopo a página inteira quebrada. - Outra chamava
useTranslationsem importar. - Numa terceira, uma função auxiliar no escopo de módulo chamava
t(). Ironia: era justamente a função criada para melhorar mensagem de erro, e ela quebrava exatamente quando havia erro.
Todos os três: vite build passava sem um aviso sequer.
A parte que me incomodou mais
Antes do linter, eu tinha escrito um verificador próprio para achar exatamente esse problema de escopo. Ele acusou uma dessas telas. Eu descartei como falso positivo, porque o mesmo verificador errava em outros arquivos e eu concluí que errava em todos.
Ele estava certo. Minha heurística com regex não sabia equilibrar chaves strings, comentários e a própria sintaxe de regex contêm { e }. O ESLint tem um analisador de escopo de verdade e não erra nisso.
A lição não é "use linter". É que eu construí uma ferramenta ruim, ela me deu a resposta certa, e eu não acreditei nela porque não confiava na ferramenta.
De brinde: duas armadilhas do Fastify
Enquanto documentava a API com OpenAPI, esbarrei em duas coisas que não são óbvias e que valem um post sozinhas.
setErrorHandler depois das rotas não funciona
await fastify.register(authRoutes, { prefix: '/api/auth' });
await fastify.register(recordRoutes, { prefix: '/api/records' });
// ... mais 30 registros
fastify.setErrorHandler((error, request, reply) => { ... }); // tarde demais
No Fastify, cada register cria um contexto encapsulado que herda o tratador de erro existente naquele momento. Definido depois, ele não alcança nenhuma rota já registrada. Não há aviso, não há erro: o padrão do framework simplesmente responde no lugar.
Comprovei com duas instâncias idênticas, mudando só a ordem:
tratador definido ANTES -> {"error":"causa real","deQuem":"projeto"}
tratador definido DEPOIS -> {"statusCode":500,"error":"Internal Server Error","message":"causa real"}
No meu caso isso significava que uma correção feita meses antes colocar a mensagem real no campo error, que é o que o frontend lê esteve inerte desde o dia em que foi feita. As rotas devolviam "Bad Request" no campo que importava, com a causa escondida em message.
Schema de response não é só documentação
Achei que declarar o schema de resposta fosse enfeite para o OpenAPI. Não é: ele controla a serialização, e o fast-json-stringify descarta o que o schema não declara. Medi antes de sair documentando:
sem schema -> { records: [...], pagination: {...} }
response estrito -> { records: [...] } <- pagination sumiu
response + additionalProperties -> { records: [...], pagination: {...} }
Documentar uma rota podia quebrar os clientes dela em silêncio. Pior: ao declarar schema para status de erro (400, 401), o corpo passou a chegar vazio {} no lugar da mensagem.
Virou teste no projeto. E eu conferi que o teste falha quando introduzo um schema perigoso, não só que passa com os atuais teste que nunca acusa nada dá falsa sensação de segurança.
Sobre as regras que deixei de fora
Duas, com o motivo escrito no próprio arquivo de configuração:
require-atomic-updatesgerava 22 acusações, todas do mesmo padrão (request.currentUser = await ...nos middlewares). Orequestdo Fastify é por requisição e os middlewares rodam em sequência a corrida que a regra descreve não existe ali.- Import de
Reactnão usado, em 66 arquivos. É dispensável com o JSX runtime automático do Vite, mas remover de todos é faxina de outro PR.
Conviver com falso positivo é como se ensina alguém a ignorar o linter. Se a regra acusa 22 coisas que não são problema, a 23ª que é passa batida.
Pelo mesmo motivo, react-hooks/exhaustive-deps ficou como aviso, não erro: são 46 ocorrências que precisam de análise caso a caso, e transformá-las em erro travaria qualquer PR no primeiro dia.
O que eu faria diferente
Colocaria o linter no primeiro dia. Não pelo estilo pela classe de bug que ele pega e que teste unitário não pega, porque exigiria renderizar cada componente e chamar cada rota.
E confiaria mais em ferramenta com analisador de verdade do que na minha própria regex.
Se quiser ver a configuração, é um projeto open source (AGPL) de base de conhecimento para incidentes: https://github.com/janeiaraujo/knowledgebase
Os arquivos são backend/eslint.config.js e frontend/eslint.config.js.