Prompts para revisão de código: 16 casos prontos
Dezesseis trechos com um defeito real em cada um e a pergunta que um revisor faria sobre ele. Cada card já fixa a linguagem, o foco da revisão de código e o ambiente em que ele roda: troque o exemplo pelo seu próprio código e toque em Aplicar.
Bugs e lógica
Código que compila e mente ao mesmo tempo: encontrar o bug pela leitura, o argumento mutável no valor padrão, o zero tratado como ausência, horas comparadas sem fuso e a regressão que entrou num pull request.
Argumento mutável por omissão
A armadilha clássica de Python: a lista na assinatura sobrevive à chamada e vai juntando dados de outros.
Reveja esta função do carrinho: os clientes encontram na encomenda artigos que nunca adicionaram.def add_item(item, cart=[]): cart.append(item) return cart
Linha 1 · crítico · bugcart=[] é criado uma única vez, quando o módulo é importado, e não em cada chamada: o segundo cliente continua a encher o carrinho do primeiro, e assim por diante até o processo reiniciar.Correção: cart: list | None = None e, como primeira linha do corpo, if cart is None: cart = [].
Um zero tomado por ausência
A verificação de verdade esconde um zero legítimo e o cliente sem saldo vê um texto de substituição.
Olhe para esta verificação: distingue mesmo um valor ausente de um zero?def render_balance(user): balance = user.get("balance") if not balance: return "sem dados" return format_money(balance)
Linha 3 · grave · bugif not balance apanha por igual None, 0 e 0.0: quem gastou até ao último cêntimo lê "sem dados" em vez de um zero honesto.Correção: if balance is None — a ausência verifica-se de forma explícita e o zero segue para o formatador como qualquer outro número.
Comparar horas sem fuso
datetime.now() devolve a hora local do servidor enquanto a validade do token fica guardada em UTC.
Verifique o cálculo da validade do token. O serviço corre em servidores de vários fusos.def is_expired(token): return datetime.now() > token.expires_at
Linha 2 · crítico · bugdatetime.now() é hora local naive e expires_at vem da base de dados em UTC: num servidor em Lisboa os tokens vivem uma hora a mais e, contra um valor aware, a comparação rebenta logo com TypeError.Correção: datetime.now(timezone.utc) e uma regra para todo o projeto — guarda-se apenas aware e em UTC.
Regressão num pull request
Uma revisão da alteração e não do ficheiro: o que vai para produção com este commit.
Reveja este diff de pull request: o que entra exatamente em produção com ele?@@ -12,8 +12,7 @@ def apply_discount(order, percent): if order.paid: raise ValueError("order already paid")- if percent > 90:- raise ValueError("discount too large") order.total = order.total * (100 - percent) / 100+ order.discount_percent = percent
Linhas 15-16 · crítico · bugA verificação apagada era o único limite de percent: com 150 o total da encomenda fica negativo e a caixa transforma isso num reembolso.Correção: repor a verificação ou passá-la para a validação do pedido, e acrescentar o caso percent=150 aos testes.
Segurança
Achar vulnerabilidade de segurança antes do deploy: a injeção de SQL no campo de busca, o token de produção esquecido dentro do script e o caminho que escapa da pasta de uploads para o resto do sistema.
Injeção de SQL na pesquisa
O endereço vindo do formulário é colado dentro do SQL: quem preenche o formulário passa a mandar na base de dados.
Reveja esta pesquisa de utilizador. O email vem diretamente de um formulário do site.def find_user(conn, email): query = "SELECT * FROM users WHERE email = '" + email + "'" return conn.execute(query).fetchone()
Linha 2 · crítico · segurançaemail é colado no texto SQL: um valor com uma plica fecha a condição e tudo o que vem a seguir é executado pela base como uma consulta sua, DROP TABLE incluído.Correção: um parâmetro em vez da concatenação — conn.execute("SELECT id, email FROM users WHERE email = %s", [email]); de caminho desaparece o SELECT * que hoje também traz o hash da palavra-passe.
Token dentro do script
Uma chave de produção está no repositório e ainda por cima é impressa no log de deploy.
Reveja este script de deploy: há aqui alguma coisa perigosa do ponto de vista da segurança?#!/usr/bin/env bashAPI_TOKEN="sk_live_9f3c1ad84b22"echo "deploying with $API_TOKEN"curl -H "Authorization: Bearer $API_TOKEN" -X POST https://api.example.com/deploy
Linhas 2-3 · crítico · segurançaUm token de produção está escrito num ficheiro versionado: está no histórico de toda a gente que clonou o repositório, e o echo copia-o ainda para o log de deploy, que muita mais gente lê.Correção: ler do ambiente ($API_TOKEN sem valor por omissão), tirar o echo e dar a chave por comprometida — ou seja, rodá-la.
Saída da pasta de uploads
O nome do ficheiro vem do pedido tal como está, e dois pontos com barra chegam a qualquer ficheiro do servidor.
Reveja este endpoint de download: o nome do ficheiro chega na query.def download(request): name = request.args.get("file") path = os.path.join("/var/app/uploads", name) return send_file(path)
Linha 3 · crítico · segurançaos.path.join sai à vontade de /var/app/uploads quando o nome é ../../etc/passwd, e um caminho absoluto descarta por completo o primeiro argumento: descarrega-se qualquer ficheiro que o processo consiga ler.Correção: os.path.basename(name), depois comparar o os.path.realpath do resultado com a pasta de uploads e só servir o ficheiro se estiver lá dentro.
Desempenho
Otimizar o desempenho antes de culpar o servidor: a consulta feita dentro do laço, que vira um N+1 silencioso, a que ignora o índice e varre a tabela toda, e o log carregado de uma vez para a memória.
Consultas dentro do ciclo
O relatório vai duas vezes à base de dados por cada utilizador: mil linhas tornam-se duas mil consultas.
Veja este relatório: com mil utilizadores demora um minuto. Onde se perde o tempo?def orders_report(user_ids): rows = [] for user_id in user_ids: user = db.query("SELECT name FROM users WHERE id = %s", user_id) orders = db.query("SELECT total FROM orders WHERE user_id = %s", user_id) rows.append((user.name, sum(o.total for o in orders))) return rows
Linhas 3-5 · grave · desempenhoDuas consultas por utilizador: mil utilizadores são duas mil idas e voltas, e o tempo vai-se na rede, não nas contas.Correção: uma consulta com JOIN e GROUP BY users.id, ou duas consultas com IN e a junção feita em memória.
Consulta que falha o índice
Uma função sobre a coluna e uma percentagem no início do LIKE desligam os índices: a base lê a tabela toda.
Reveja esta consulta: numa tabela de dez milhões de linhas demora vinte segundos.SELECT *FROM ordersWHERE date_trunc('day', created_at) = '2026-09-01' AND lower(email) LIKE '%@example.com'ORDER BY created_at DESC
Linhas 3-4 · grave · desempenhodate_trunc sobre created_at deixa o índice inútil e um LIKE começado por percentagem não o consegue usar de todo: sobra um seq scan pela tabela inteira, e o SELECT * ainda arrasta colunas que ninguém lê.Correção: comparar created_at com um intervalo (a partir da meia-noite de 1 de setembro e menor do que a meia-noite do dia 2), indexar o domínio à parte ou guardá-lo numa coluna própria, e selecionar só os campos usados.
O log inteiro em memória
O ficheiro é lido de uma vez, por isso o tamanho do log passa a ser o tamanho do processo.
Reveja este contador de erros num log. Os ficheiros chegam a vários gigabytes.def count_errors(path): lines = open(path).read().split("\n") return len([line for line in lines if "ERROR" in line])
Linha 2 · grave · desempenhoread() traz o ficheiro inteiro para memória, o split duplica a conta e a lista dentro de len() guarda uma terceira cópia: num log de oito gigabytes chega primeiro o OOM killer do que o resultado. Já agora, o ficheiro nunca é fechado.Correção: with open(path) as f e sum(1 for line in f if "ERROR" in line) — linha a linha e com memória constante.
Legibilidade e padrões
Deixar o código mais legível para quem vem depois: quatro condições aninhadas que dá para achatar, nomes de uma letra contra o guia de estilo e a mesma alíquota de imposto duplicada em duas funções.
Quatro condições encaixadas
A regra de envio do email esconde-se no quinto nível de indentação e só se lê de uma vez.
Avalie a legibilidade desta função: é preciso lê-la até ao fim para saber a condição.def notify(user): if user is not None: if user.email is not None: if user.subscribed: if not user.banned: send_email(user.email) return True return False
Linhas 2-5 · menor · legibilidadeQuatro if encaixados são uma só regra estendida em escada: para saber quem recebe o email é preciso segurar as quatro condições ao mesmo tempo, e a PEP 8 pede a forma plana.Correção: saídas antecipadas — if user is None: return False e por aí fora — e o corpo fica num único nível de indentação.
Nomes contra o guia de estilo
Um método com maiúscula inicial e variáveis de uma letra: queixa-se o RuboCop e queixa-se quem ler a seguir.
Reveja este método com o RuboCop: o que é que aqui contraria o estilo habitual de Ruby?def CalcTotal(o) t = 0 o.each do |i| t = t + i.price * i.qty end tend
Linhas 1-4 · menor · padrõesNaming/MethodName: em Ruby os nomes de métodos são snake_case e o CamelCase lê-se aqui como uma constante. Os nomes o, t e i não dizem nada sobre o conteúdo, e somar à mão é exatamente o trabalho do sum.Correção: def calc_total(items) e items.sum do |item| item.price * item.qty end — quatro linhas passam a uma.
IVA em dois sítios
A mesma regra copiada para duas funções e já divergente: 20 por cento na fatura, 19 no recibo.
Reveja estas duas funções: calculam a mesma coisa?def invoice_total(order): return round(order.subtotal * 1.2, 2)def receipt_total(order): return round(order.subtotal * 1.19, 2)
Linhas 2 e 5 · grave · arquiteturaUma regra de negócio escrita duas vezes já divergiu: a fatura aplica 20 por cento e o recibo 19, por isso o cliente vê dois totais diferentes para a mesma encomenda.Correção: uma constante VAT_RATE e uma função usada pelas duas — assim a taxa muda num único sítio.
Testes e fiabilidade
O que acontece quando algo falha: testes automatizados que só cobrem o caminho feliz, a exceção capturada e engolida sem nenhum registro e o backup que nunca foi restaurado nem uma vez para teste.
Teste só do caminho feliz
Um único caso verde dá a sensação de uma cobertura que não existe.
Avalie estes testes de apply_discount: o que lhes falta?def test_apply_discount(): order = Order(total=100) apply_discount(order, 10) assert order.total == 90
Linha 1 · grave · testesEstá coberto um caso: um desconto normal numa encomenda por pagar. Nada sobre zero, sobre cem por cento, sobre um valor negativo, sobre uma encomenda já paga ou sobre o arredondamento de 33,33 — qualquer um desses ramos pode partir sem ninguém dar por isso.Correção: parametrize nos valores-limite e um teste próprio com pytest.raises para percent=150 e para a encomenda já paga.
Exceção engolida
except Exception: pass transforma uma falha em "ok" e apaga o rasto dos logs.
Reveja esta gravação do perfil: os utilizadores dizem que às vezes as alterações desaparecem.def save_profile(user, data): try: db.update(user.id, data) search.reindex(user.id) except Exception: pass return "ok"
Linhas 5-6 · crítico · bugexcept Exception: pass engole tudo e devolve "ok" mesmo quando a escrita não aconteceu: o utilizador vê sucesso, os dados não estão lá e o log está vazio. Com duas operações dentro do mesmo try, uma falha da reindexação deixa de se distinguir de uma falha da base.Correção: apanhar as exceções concretas, registar com logger.exception e devolver um estado honesto; tirar a reindexação para fora, para que a falha dela não anule a gravação.
Backup sem verificação
O script ignora os códigos de saída e apaga as cópias antigas antes de alguém descobrir que a nova está vazia.
Reveja este script de backup noturno: a reposição a partir da última cópia não funcionou.#!/usr/bin/env bashpg_dump "$DATABASE_URL" > /backup/db.sqlgzip -f /backup/db.sqlfind /backup -name "db.sql.gz" -mtime +7 -delete
Linhas 2-4 · crítico · bugFalta o set -euo pipefail: se o pg_dump falhar o ficheiro é criado à mesma, vazio, o gzip comprime-o sem se queixar e o find apaga as cópias boas com mais de uma semana; ao fim de sete dias não sobra uma inteira. Um $DATABASE_URL por definir passa igualmente despercebido.Correção: set -euo pipefail na primeira linha, verificar o tamanho do dump depois do pg_dump e só apagar as cópias antigas depois de a nova correr bem.