dominio: reglas rust
Async es async en toda la cadena [R:async-all-the-way]
Parcialmente reforzado por hook:no-block-on-in-async; cualquier otra forma en que un hilo del runtime se bloquea dentro de async -- I/O síncrono de archivos y red, un lock
std::syncmantenido a través de un await, una sección larga de CPU que nunca se pasa aspawn_blocking-- y cualquier archivo Rust fuera de un árbolsrc/se rige solo por esta instrucción.
No uses block_on dentro de código async. Un runtime async multiplexa muchas tareas en pocos hilos, por lo que una llamada bloqueante no retrasa una tarea -- elimina un trabajador del pool durante la duración y retrasa cada tarea que se habría ejecutado allí. Las víctimas no están relacionadas con el código que bloqueó, por eso el síntoma es una latencia de cola inexplicable en otro lugar, y por eso es casi imposible de atribuir después del hecho.
block_on solo es correcto en el límite donde el código síncrono entra en async: main, un test, un callback de una biblioteca C. Una vez dentro, quédate dentro -- I/O async, un lock consciente de async donde sea que una guardia deba sobrevivir a un await, y spawn_blocking para trabajo que realmente bloquea, como cálculo intensivo de CPU o un cliente síncrono de terceros.
Lo mismo se aplica a una guardia std::sync::Mutex mantenida a través de un await. Nada la marca como bloqueante, pero mantiene un lock mientras la tarea está desprogramada, por lo que la tarea contendiente bloquea su propio hilo de trabajo y el fallo se presenta como un deadlock en lugar de como un lock.
Un block_on anidado en un runtime de hilo actual no se degrada -- deadlockea directamente. Ese es el fallo honesto; el caso multi-hilo simplemente oculta el mismo error detrás de un número de hilos, hasta que la carga elimina el escondite.
Toma un préstamo en la firma a menos que la función necesite poseer [R:borrow-in-signatures]
Toma &str en lugar de String, &[T] en lugar de Vec<T>, &Path en lugar de PathBuf. Una firma es una declaración sobre lo que la función necesita, y un parámetro propio dice "me quedaré con esto" -- así que cuando el cuerpo solo lee, la firma es falsa y cada llamador paga por ello en una asignación.
Posee el parámetro exactamente donde la función lo almacena. Allí, impl Into<String> es la forma cortés: un llamador que tiene una String la mueve sin costo, y un llamador que tiene un &str asigna una vez, a sabiendas. A la salida, devuelve &str en lugar de &String -- la indirección extra no le da nada al llamador y fija la representación del campo en la API pública.
El costo rara vez es la asignación única. Es que un parámetro propio se propaga: el llamador clona para satisfacerlo, su llamador clona para satisfacer eso, y una firma elegida sin pensar se convierte en una columna de clones que cada uno parece localmente necesario. R:justify-every-clone es donde esos clones salen a la superficie; esta regla es cómo nunca se crean.
Diseña escribiendo los tipos primero, antes de cualquier lógica [R:design-types-first]
Ha ocurrido 2 vez/veces desde que se escribió; más recientemente 2026-11-01.
Dibuja los tipos hasta que el diseño emerja, luego escribe la lógica. El boceto a nivel de tipos es la especificación ejecutable que una fase roja de test-first busca, y es la más fuerte: un test muestrea puntos del espacio de comportamiento, un tipo restringe todo el espacio y el compilador lo prueba en todas partes, en tiempo de compilación, mientras exista el código. Cuando los tipos son correctos, gran parte de la implementación se escribe sola, y muchas implementaciones incorrectas dejan de compilar. En Rust, este orden reemplaza cualquier valor predeterminado test-first -- los tests no se eliminan, se degradan a la capa donde son la herramienta adecuada: tests de propiedades para leyes de comportamiento que los tipos no pueden codificar, tests unitarios como anclas de regresión para errores pasados. Aplica cada garantía en la capa más fuerte que pueda sostenerla, y al revisar, pregunta primero no "pasa" sino "cuál de estas garantías podría subir una capa?"
Los enums de error llevan los valores que identifican el fallo [R:errors-name-what-failed]
Parcialmente reforzado por hook:no-string-error; variantes sin campos, variantes que omiten el valor de identificación, y un catch-all llamado
Unknown(String), que una línea no puede distinguir de una variante que lleva el valor no reconocido, se rige solo por esta instrucción.
Cada fallo distinto es una variante de un enum thiserror, y cada variante lleva los valores que identifican la instancia:
#[derive(Debug, thiserror::Error)]
pub enum ConnectError {
#[error("Falló TCP connect a {addr}")]
Tcp { addr: SocketAddr, #[source] source: io::Error },
#[error("tiempo de espera agotado después de {elapsed:?}")]
Timeout { elapsed: Duration },
}
Dos cosas se derivan de esto que una cadena no puede dar. El llamador puede hacer match -- reintentar un Timeout, mostrar un Tcp -- en lugar de analizar prosa que cambia la próxima vez que alguien edita un mensaje. Y el mensaje nombra la dirección real, por lo que una línea de log es suficiente para actuar en lugar del inicio de una investigación.
Mantén la causa en #[source] en lugar de interpolar en el texto. La cadena entonces se imprime una vez, en el borde, sin que cada capa reestablezca la capa debajo de ella.
Una variante por condición que un llamador podría tratar de manera plausible de forma diferente. Colapsar cuatro causas en Other(String) recrea el error de cadena dentro de un enum: se lee como un tipo y se comporta como prosa, y es la forma que esta regla existe para detectar.
Cada clone() lleva su razón, o el diseño está mal [R:justify-every-clone]
Parcialmente reforzado por hook:no-clone-without-comment; si el comentario indica una razón real en lugar de repetir la llamada, y cualquier archivo Rust fuera de un árbol
src/se rige solo por esta instrucción.
Un error del borrow-checker es una pregunta sobre propiedad. clone() no la responde; paga para evitar responderla. Prueba las respuestas primero: reestructura para que un propietario sea obvio, toma un préstamo con una vida útil nombrada, divide el préstamo en campos más pequeños, o comparte con Arc/Rc donde el valor es genuinamente compartido en lugar de copiado.
Donde un clone sobrevive a ese examen, escribe la razón a su lado, en la forma de la casa utilizada en todo este código base:
let sources: Vec<RuleTag> = rules.iter().map(|r| r.tag().clone()).collect(); // allow:clone: el OutputFile es dueño de su procedencia, sobreviviendo al préstamo &Library
El comentario es lo que hace que la regla sea revisable. Los clones no anotados son indistinguibles entre sí, por lo que un revisor tiene que re-derivar el argumento de propiedad para cada uno y en la práctica no lo re-deriva para ninguno. Un clone anotado indica una afirmación que se puede verificar, y que se puede encontrar y eliminar el día en que el diseño a su alrededor cambie.
La razón debe ser sobre propiedad o vida útil. "Para que compile" es la clase de error reexpresada, no una justificación. Dos copias de un valor que el código cree que es un valor es un error de corrección esperando a quien mute la incorrecta.
La mayoría de los clones que sobreviven a la revisión fueron creados por una firma, no por un sitio de llamada: R:borrow-in-signatures es de donde viene la presión, y arreglar el parámetro generalmente elimina el clone en lugar de anotarlo.
Los módulos son el límite de encapsulación; pub es una exportación deliberada [R:module-visibility-is-deliberate]
Usa pub(crate) y pub(super) libremente; reserva pub simple para lo que el crate exporta deliberadamente. La visibilidad no es papeleo. Es la declaración de lo que todavía se puede cambiar libremente, y es la única declaración que un compilador puede verificar.
El valor predeterminado importa porque pub es barato de agregar y caro de eliminar. Una vez que un elemento es público, un llamador externo puede depender de él, por lo que el módulo ya no se puede reorganizar, el campo ya no se puede renombrar, y el helper escrito para un solo sitio de llamada ahora es una API compatible. Nada de eso se decidió; se estableció por defecto.
Un módulo mantiene un invariante de la misma manera que un tipo. Un helper que solo puede ser llamado después de una verificación pertenece junto a esa verificación, privado al módulo, por lo que "solo después" se aplica porque nadie más puede llamarlo en absoluto. Hacerlo público convierte esa garantía en un comentario de documentación.
Re-exporta la superficie deseada explícitamente en la raíz del crate con pub use, y deja que todo lo demás sea tan privado como pueda ser. La API pública es entonces una lista que alguien escribió, en lugar del residuo de dónde el código vivía.
R:private-fields-only es el mismo argumento un nivel más abajo, en el campo en lugar del elemento. Los dos son separables -- un campo privado en un tipo pub filtra estructura, un tipo pub(crate) con campos públicos no filtra -- por lo que satisfacer uno no dice nada sobre el otro.
Marca los valores de retorno consecuenciales con #[must_use] [R:must-use-on-consequential-returns]
Pon #[must_use] en cada función que devuelve un Result, y en cada función cuyo valor de retorno es el punto de llamarla. El compilador entonces marca un resultado descartado en cada sitio de llamada, presente y futuro, en lugar de dejarlo a un revisor para notar una declaración vacía entre cien.
Prefiere el atributo en el tipo sobre el atributo en la función. #[must_use] struct Recibo; viaja a cada función que devuelve un Recibo, incluyendo las escritas más tarde por alguien que nunca leyó esta regla; el atributo a nivel de función debe ser recordado cada vez. Adjúntalo al tipo cuando el tipo siempre sea consecuente, y recurre a la función solo para el caso más estrecho donde el mismo tipo a veces vale la pena descartar.
Eleva la advertencia resultante a un error en CI. Un #[must_use] cuya violación imprime una nota que nadie lee es un comentario con sintaxis extra, y la garantía que afirma no se sostiene en ninguna parte.
Newtype liberalmente: conceptos distintos obtienen tipos distintos [R:newtype-liberally]
Dale a cada concepto de dominio su propio tipo, incluso cuando la representación subyacente sea idéntica. Millas(f64) y Kilómetros(f64), Host(String) y Puerto(u16) -- nunca dos f64 simples o una String y un u16 cuyo orden solo un humano recuerda. Los newtypes son normalmente de costo cero: se compilan al mismo código de máquina que el primitivo, por lo que lo único que agregan es el error de compilación que deseas -- pero donde ese costo es relevante, R:verify-the-abstraction-compiled-away requiere que la afirmación sea verificada en lugar de repetida. La mezcla de unidades que no se puede escribir es más barata que la atrapada en revisión, y mucho más barata que la que no se atrapa. R:parse-dont-validate es de donde viene el newtype -- el límite lo acuña como un testigo; esta regla se trata de llevarlo a todas partes después en lugar de desenvolverlo de vuelta al primitivo.
No anyhow en tipos de retorno de librerías [R:no-anyhow-in-libraries]
También reforzado por hook:no-anyhow-in-lib.
Los crates de librería devuelven enums de error tipados (thiserror), por lo que un Result dice exactamente qué puede salir mal y los llamadores pueden hacer match sobre él. anyhow pertenece solo en el borde binario más externo. Esta regla ha sido graduada: el hook no-anyhow-in-lib ahora la refuerza deterministicamente en tiempo de escritura.
No silencies una variable local no utilizada con let _ =; úsala, repórtala o elimínala [R:no-let-underscore-on-a-local]
Parcialmente reforzado por hook:no-let-underscore-local; los enlaces con prefijo de guion bajo se rigen solo por esta instrucción.
La advertencia del compilador de enlace no utilizado es un lector. Dice que un valor que te tomaste la molestia de calcular no llega a nada. let _ = x; existe para apagar ese lector, y en un local casi siempre lo apaga en el momento en que era correcto.
Toma dos formas, y ambas compilan limpias con -D warnings mientras se leen a un revisor como una decisión que alguien tomó:
- Una distinción calculada y descartada. Se analiza una bandera, se decodifica un modo, se lee un valor de configuración -- y luego se descarta, por lo que los casos que se suponía que separara se manejan silenciosamente como uno.
- Un fallo atrapado y descartado.
if let Err(e) = op() { let _ = e; continue; }no deja ningún evento en ninguna parte; el fallo ocurrió, y nada en el sistema puede decirlo después.
Cuando la advertencia se activa, haz una de tres cosas. Usa el valor -- una distinción no utilizada generalmente significa que falta una rama, y la advertencia la está señalando. Repórtalo o propágalo -- un error que es genuinamente seguro ignorar aún obtiene un evento nombrado, o un comentario que diga por qué nadie downstream necesita saberlo. O elimina el código que lo calculó. Donde un enlace está muerto por diseño, como la última asignación a una variable de bucle, reescribe el bucle para que la asignación muerta nunca ocurra en lugar de silenciar la advertencia por ello.
let _ = f(); en una llamada es un caso diferente: descarta un valor de retorno, que R:must-use-on-consequential-returns gobierna. Esta regla es sobre un local que ya tienes. R:guarantee-needs-a-reader depende del compilador para atrapar enlaces no leídos; let _ = es la única ortografía que no puede ver.
Verificación de modo de fallo, cada vez que estés a punto de escribir let _ = nombre;: para qué se calculó este valor para decidir, y dónde ocurre ahora esa decisión? Si la respuesta es "en ninguna parte", la advertencia tenía razón.
No unwrap() en código de producción [R:no-unwrap-in-production]
También reforzado por hook:no-unwrap-in-src + hook:no-expect-empty-msg.
No uses unwrap() en código de producción. Usa expect() solo con un mensaje de pánico significativo que nombre el recurso y el invariante, o devuelve el error con ? y deja que el llamador decida cómo mostrarlo. Esta regla ha sido graduada: el hook no-unwrap-in-src ahora la refuerza deterministicamente en tiempo de escritura, por lo que la capa de instrucción ya no tiene que hacerlo.
Analiza, no valides [R:parse-dont-validate]
Parcialmente reforzado por hook:no-free-validator; volver a verificar datos analizados en sitios de llamada internos se rige solo por esta instrucción.
Transforma la entrada sin procesar en un tipo de dominio rico en el límite más externo, produciendo un newtype testigo cuya existencia prueba que la verificación ocurrió. El código interno toma el testigo y nunca vuelve a verificar -- un perímetro, una verificación, aplicada en todas partes después por el compilador. R:parse-wide-then-range-check afila esto: el límite debe poder ver el valor ilegal para nombrarlo.
Analiza amplio, luego verifica rango [R:parse-wide-then-range-check]
Analiza a un tipo lo suficientemente amplio como para representar el valor fuera de rango, luego verifica rango
...
[truncado para traducción]