domínio: regras Rust
Async é async em toda a cadeia [R:async-all-the-way]
Parcialmente aplicado pelo hook:no-block-on-in-async; toda outra forma de uma thread do runtime ser bloqueada dentro de async -- I/O síncrono de arquivo e rede, um lock
std::syncmantido através de umawait, uma seção longa de CPU nunca passada paraspawn_blocking-- e qualquer arquivo Rust fora de uma árvoresrc/é mantido por esta instrução sozinha.
Não use block_on dentro de código async. Um runtime async multiplexa muitas tarefas em poucas threads, então uma chamada bloqueante não atrasa uma tarefa -- ela remove um trabalhador do pool por toda a duração e atrasa todas as tarefas que teriam sido executadas ali. As vítimas não estão relacionadas ao código que bloqueou, e é por isso que o sintoma é latência final inexplicada em outro lugar, e por que é quase impossível de atribuir depois do fato.
block_on é correto apenas na fronteira onde código síncrono entra em async: main, um teste, um callback de uma biblioteca C. Uma vez dentro, permaneça dentro -- I/O async, um lock ciente de async onde quer que um guard deva sobreviver a um await, e spawn_blocking para trabalho que genuinamente bloqueia, como computação vinculada à CPU ou um cliente síncrono de terceiros.
O mesmo se aplica a um guard de std::sync::Mutex mantido através de um await. Nada o sinaliza como bloqueante, mas ele mantém um lock enquanto a tarefa é desscalonada, então a tarefa contendente bloqueia sua própria thread de trabalhador e a falha se apresenta como um deadlock em vez de um lock.
block_on aninhado em um runtime de thread atual não degrada -- ele deadlocka diretamente. Essa é a falha honesta; o caso multi-threaded meramente esconde o mesmo erro atrás de uma contagem de threads, até que a carga remova o esconderijo.
Use um empréstimo na assinatura a menos que a função precise ser proprietária [R:borrow-in-signatures]
Use &str em vez de String, &[T] em vez de Vec<T>, &Path em vez de PathBuf. Uma assinatura é uma declaração sobre o que a função precisa, e um parâmetro owned diz "Eu vou manter isso" -- então quando o corpo apenas lê, a assinatura é falsa e cada caller paga por isso em uma alocação.
Seja proprietário do parâmetro exatamente onde a função o armazena. Lá, impl Into<String> é a forma cortês: um caller segurando uma String a move sem custo, e um caller segurando um &str aloca uma vez, conscientemente. Na saída, retorne &str em vez de &String -- a indireção extra não traz benefício ao caller e fixa a representação do campo na API pública.
O custo raramente é a alocação única. É que um parâmetro owned se propaga: o caller clona para satisfazê-lo, seu caller clona para satisfazer isso, e uma assinatura escolhida sem pensar se torna uma coluna de clones que cada um parece localmente necessário. R:justify-every-clone é onde esses clones vêm à tona; esta regra é como eles nunca são criados.
Projete escrevendo os tipos primeiro, antes de qualquer lógica [R:design-types-first]
Ocorreu 2 vez(es) desde que foi escrita; mais recentemente em 2026-11-01.
Esboce os tipos até que o design surja, então escreva a lógica. O esboço em nível de tipo é a especificação executável que uma fase vermelha de test-first está buscando, e é a mais forte: um teste amostra pontos do espaço de comportamento, um tipo restringe todo o espaço e o compilador prova isso em todos os lugares, em tempo de compilação, enquanto o código existir. Quando os tipos estão certos, grande parte da implementação se escreve sozinha, e muitas implementações erradas param de compilar. Em Rust, esta ordenação substitui qualquer padrão test-first padrão -- testes não são removidos, eles são relegados à camada onde são a ferramenta certa: testes de propriedade para leis comportamentais que os tipos não podem codificar, testes unitários como âncoras de regressão para bugs passados. Aplique cada garantia na camada mais forte que possa mantê-la, e ao revisar, pergunte primeiro não "passa?" mas "qual dessas garantias poderia subir uma camada?"
Enums de erro carregam os valores que identificam a falha [R:errors-name-what-failed]
Parcialmente aplicado pelo hook:no-string-error; variantes sem campos, variantes omitindo o valor identificador, e um catch-all chamado Unknown(String), que uma linha não consegue distinguir de uma variante carregando o valor não reconhecido, é mantido por esta instrução sozinha.
Cada falha distinta é uma variante de um enum thiserror, e cada variante carrega os valores que identificam a instância:
#[derive(Debug, thiserror::Error)]
pub enum ConnectError {
#[error("Falha na conexão TCP para {addr}")]
Tcp { addr: SocketAddr, #[source] source: io::Error },
#[error("tempo limite excedido após {elapsed:?}")]
Timeout { elapsed: Duration },
}
Duas coisas se seguem que uma string não pode dar. O caller pode match -- repetir um Timeout, exibir um Tcp -- em vez de analisar prosa que muda na próxima vez que alguém editar uma mensagem. E a mensagem nomeia o endereço real, então uma única linha de log é suficiente para agir em vez do início de uma investigação.
Mantenha a causa em #[source] em vez de interpolá-la no texto. A cadeia então imprime uma vez, na borda, sem que cada camada repita a camada abaixo dela.
Uma variante por condição que um caller possa plausivelmente tratar de forma diferente. Colapsar quatro causas em Other(String) recria o erro de string dentro de um enum: ele se lê como um tipo e se comporta como prosa, e é a forma que esta regra existe para capturar.
Cada clone() carrega sua razão, ou o design está errado [R:justify-every-clone]
Parcialmente aplicado pelo hook:no-clone-without-comment; se o comentário declara uma razão real em vez de reafirmar a chamada, e qualquer arquivo Rust fora de uma árvore
src/é mantido por esta instrução sozinha.
Um erro do borrow-checker é uma pergunta sobre ownership. clone() não a responde; ele paga para evitar respondê-la. Tente as respostas primeiro: reestruture para que um proprietário seja óbvio, use um empréstimo com um lifetime nomeado, divida o empréstimo em campos menores, ou compartilhe com Arc/Rc onde o valor é genuinamente compartilhado em vez de copiado.
Onde um clone sobrevive a esse exame, escreva a razão ao lado dele, na forma da casa usada em todo este codebase:
let sources: Vec<RuleTag> = rules.iter().map(|r| r.tag().clone()).collect(); // permitir:clone: o OutputFile possui sua proveniência, sobrevivendo ao empréstimo &Library
O comentário é o que torna a regra revisável. Clones não anotados são indistinguíveis uns dos outros, então um revisor tem que re-derivar o argumento de ownership para cada um e na prática re-deriva para nenhum. Um clone anotado declara uma afirmação que pode ser verificada, e que pode ser encontrada e removida no dia em que o design ao redor dela mudar.
A razão deve ser sobre ownership ou lifetime. "Para fazê-lo compilar" é a classe de erro reafirmada, não uma justificativa. Duas cópias de um valor que o código acredita ser um único valor é um bug de correção esperando por quem mutar o errado.
A maioria dos clones que sobrevive à revisão foi criada por uma assinatura, não por um ponto de chamada: R:borrow-in-signatures é de onde a pressão vem, e corrigir o parâmetro geralmente deleta o clone em vez de anotá-lo.
Módulos são a fronteira de encapsulamento; pub é uma exportação deliberada [R:module-visibility-is-deliberate]
Use pub(crate) e pub(super) livremente; reserve pub simples para o que a crate deliberadamente exporta. Visibilidade não é papelada. É a declaração do que ainda pode ser mudado livremente, e é a única tal declaração que um compilador pode verificar.
O padrão importa porque pub é barato de adicionar e caro de remover. Uma vez que um item é público, um caller externo pode depender dele, então o módulo não pode mais ser reorganizado, o campo não pode ser renomeado, e o helper escrito para um único ponto de chamada é agora uma API suportada. Nada disso foi decidido; foi padronizado.
Um módulo mantém um invariante da mesma forma que um tipo. Um helper que só pode ser chamado após uma verificação pertence ao lado dessa verificação, privado ao módulo, de modo que "somente após" é aplicado por ninguém mais poder chamá-lo. Torná-lo público converte essa garantia em um comentário de documentação.
Re-exporte a superfície pretendida explicitamente na raiz da crate com pub use, e deixe tudo atrás dela tão privado quanto possível. A API pública é então uma lista que alguém escreveu, em vez do resíduo de onde o código aconteceu de estar.
R:private-fields-only é o mesmo argumento um nível abaixo, no campo em vez do item. Os dois são separáveis -- um campo privado em um tipo pub vaza estrutura, um tipo pub(crate) com campos públicos não vaza -- então satisfazer um não diz nada sobre o outro.
Marque valores de retorno consequenciais com #[must_use] [R:must-use-on-consequential-returns]
Coloque #[must_use] em toda função que retorna um Result, e em toda função cujo valor de retorno é o ponto de chamá-la. O compilador então sinaliza um resultado descartado em cada ponto de chamada, presente e futuro, em vez de deixar para um revisor notar uma declaração solitária entre cem.
Prefira o atributo no tipo em vez do atributo na função. #[must_use] struct Receipt; viaja para toda função que retorna um Receipt, incluindo aquelas escritas depois por alguém que nunca leu esta regra; o atributo em nível de função precisa ser lembrado cada vez. Anexe-o ao tipo sempre que o tipo for sempre consequencial, e recorra à função apenas para o caso mais restrito onde o mesmo tipo às vezes vale a pena ser descartado.
Eleve o aviso resultante a um erro no CI. Um #[must_use] cuja violação imprime uma nota que ninguém lê é um comentário com sintaxe extra, e a garantia que ele reivindica não é mantida em lugar nenhum.
Newtype liberalmente: conceitos distintos recebem tipos distintos [R:newtype-liberally]
Dê a cada conceito de domínio seu próprio tipo, mesmo quando a representação subjacente é idêntica. Miles(f64) e Kilometers(f64), Host(String) e Port(u16) -- nunca dois f64 simples ou uma String e um u16 cuja ordem apenas um humano lembra. Newtypes são normalmente de custo zero: eles compilam para o mesmo código de máquina que o primitivo, então a única coisa que adicionam é o erro de compilação que você quer -- mas onde esse custo é crítico, R:verify-the-abstraction-compiled-away requer que a afirmação seja verificada em vez de repetida. A mistura de unidades que não pode ser escrita é mais barata do que a capturada em revisão, e muito mais barata do que a que não é. R:parse-dont-validate é de onde o newtype vem -- a fronteira o cunha como uma testemunha; esta regra é sobre carregá-lo em todos os lugares depois, em vez de desembrulhar de volta ao primitivo.
Nenhum anyhow em tipos de retorno de biblioteca [R:no-anyhow-in-libraries]
Também aplicado pelo hook:no-anyhow-in-lib.
Crates de biblioteca retornam enums de erro tipados (thiserror), para que um Result diga exatamente o que pode dar errado e os callers possam fazer match. anyhow pertence apenas na borda binária mais externa. Esta regra foi graduada: o hook no-anyhow-in-lib agora a aplica deterministicamente no momento da escrita.
Não silencie uma variável local não usada com let _ =; use-a, reporte-a ou exclua-a [R:no-let-underscore-on-a-local]
Parcialmente aplicado pelo hook:no-let-underscore-local; vinculações com prefixo underscore são mantidas por esta instrução sozinha.
O aviso de vinculação não usada do compilador é um leitor. Ele diz que um valor que você teve o trabalho de computar não chega a lugar nenhum. let _ = x; existe para desligar esse leitor, e em uma variável local quase sempre o desliga no momento em que estava certo.
Ele assume duas formas, e ambas compilam limpas sob -D warnings enquanto se leem para um revisor como uma decisão que alguém tomou:
- Uma distinção computada e descartada. Uma flag é analisada, um modo é decodificado, um valor de config é lido -- e então descartado, de modo que os casos que deveriam ser separados são tratados silenciosamente como um.
- Uma falha capturada e descartada.
if let Err(e) = op() { let _ = e; continue; }não deixa nenhum evento em lugar nenhum; a falha aconteceu, e nada no sistema pode dizer isso depois.
Quando o aviso dispara, faça uma de três coisas. Use o valor -- uma distinção não usada geralmente significa que um branch está faltando, e o aviso está apontando para ele. Reporte ou propague-o -- um erro que é genuinamente seguro ignorar ainda recebe um evento nomeado, ou um comentário dizendo por que ninguém downstream precisa saber. Ou exclua o código que o computou. Onde uma vinculação está morta por design, como a última atribuição a uma variável de loop, reescreva o loop para que a atribuição morta nunca aconteça em vez de silenciar o aviso sobre ela.
let _ = f(); em uma chamada é um caso diferente: descarta um valor de retorno, que R:must-use-on-consequential-returns governa. Esta regra é sobre uma variável local que você já possui. R:guarantee-needs-a-reader depende do compilador para capturar vinculações não lidas; let _ = é a única grafia que ele não pode ver.
Verificação de modo de falha, sempre que você estiver prestes a escrever let _ = nome;: para que este valor foi computado para decidir, e onde essa decisão agora acontece? Se a resposta for "em lugar nenhum", o aviso estava certo.
Nenhum unwrap() em código de produção [R:no-unwrap-in-production]
Também aplicado pelo hook:no-unwrap-in-src + hook:no-expect-empty-msg.
Não use unwrap() em código de produção. Use expect() apenas com uma mensagem de pânico significativa que nomeie o recurso e o invariante, ou retorne o erro com ? e deixe o caller decidir como exibi-lo. Esta regra foi graduada: o hook no-unwrap-in-src agora a aplica deterministicamente no momento da escrita, então a camada de instrução não precisa mais fazer isso.
Parseie, não valide [R:parse-dont-validate]
Parcialmente aplicado pelo hook:no-free-validator; re-verificar dados analisados em pontos de chamada internos é mantido por esta instrução sozinha.
Transforme a entrada bruta em um tipo de domínio rico na fronteira mais externa, produzindo um newtype testemunha cuja existência prova que a verificação aconteceu. O código interior pega a testemunha e nunca re-verifica -- um perímetro, uma verificação, aplicada em todos os lugares depois pelo compilador. R:parse-wide-then-range-check afia isso: a fronteira deve ser capaz de ver o valor ilegal para nomeá-lo.
Parseie amplo, então verifique a faixa [R:parse-wide-then-range-check]
Parseie em um tipo amplo o suficiente para representar o valor fora da faixa, então verifique a faixa para ...
[truncado para tradução]