진단 속성

진단 속성 (Diagnostic attributes)

아래 속성들은 컴파일 중에 진단(diagnostic) 메시지를 제어하거나 생성하는 데 사용돼요. 컴파일러가 어떤 경고나 에러를 내보낼지, 그리고 그 메시지를 어떻게 다듬을지를 다루는 속성들이에요.

출처: Rust Reference - Diagnostic attributes

본문

린트 검사 속성

린트 검사(lint check) 는 도달할 수 없는 코드나 빠진 문서화 같은 잠재적으로 바람직하지 않은 코딩 패턴에 이름을 붙인 것이에요.

린트 속성 allow, expect, warn, deny, forbid는 속성이 적용되는 개체의 린트 레벨을 바꿀 린트 이름 목록을 지정하기 위해 MetaListPaths 구문을 사용해요.

어떤 린트 검사 C에 대해:

  • #[allow(C)] — C에 대한 검사를 무시(위반해도 보고 안 됨)하도록 덮어써요.
  • #[expect(C)] — 린트 C가 내보내질 것으로 예상함을 나타내요. 이 속성은 C의 발생을 억제하거나, 기대가 채워지지 않으면 경고를 내요.
  • #[warn(C)] — C의 위반에 대해 경고하지만 컴파일은 계속해요.
  • #[deny(C)] — C의 위반을 만난 후 에러를 신호해요.
  • #[forbid(C)]deny(C)와 같지만, 이후에 린트 레벨을 바꾸는 것도 금지해요.

rustc가 지원하는 린트 검사는 rustc -W help로 기본 설정과 함께 찾을 수 있고, rustc book에 문서화되어 있어요.

#![allow(unused)]
fn main() {
pub mod m1 {
    // 여기서는 누락된 문서가 무시돼요
    #[allow(missing_docs)]
    pub fn undocumented_one() -> i32 { 1 }

    // 여기서는 누락된 문서가 경고를 신호해요
    #[warn(missing_docs)]
    pub fn undocumented_too() -> i32 { 2 }

    // 여기서는 누락된 문서가 에러를 신호해요
    #[deny(missing_docs)]
    pub fn undocumented_end() -> i32 { 3 }
}
}

린트 속성은 이전 속성에서 지정한 레벨을 덮어쓸 수 있어요. 단, 금지된(forbid) 린트를 바꾸려는 시도는 안 돼요(deny는 forbid 컨텍스트 안에서 허용되지만 무시되는 예외가 있어요). 이전 속성이란 구문 트리에서 더 높은 레벨의 속성, 또는 왼쪽에서 오른쪽의 소스 순서로 같은 개체에 있는 이전 속성을 말해요.

아래 예시는 allowwarn을 써서 특정 검사를 켜고 끄는 방법을 보여줘요.

#![allow(unused)]
fn main() {
#[warn(missing_docs)]
pub mod m2 {
    #[allow(missing_docs)]
    pub mod nested {
        // 여기서는 누락된 문서가 무시돼요
        pub fn undocumented_one() -> i32 { 1 }

        // 위의 allow에도 불구하고 여기서는
        // 누락된 문서가 경고를 신호해요.
        #[warn(missing_docs)]
        pub fn undocumented_two() -> i32 { 2 }
    }

    // 여기서는 누락된 문서가 경고를 신호해요
    pub fn undocumented_too() -> i32 { 3 }
}
}

아래 예시는 forbid를 써서 그 린트 검사에 대한 allowexpect의 사용을 금지하는 방법을 보여줘요.

#![allow(unused)]
fn main() {
#[forbid(missing_docs)]
pub mod m3 {
    // 이 경고 토글 시도는 여기서 에러를 신호해요
    #[allow(missing_docs)]
    /// Returns 2.
    pub fn undocumented_too() -> i32 { 2 }
}
}

rustc는 명령줄에서 린트 레벨을 설정할 수 있고, 보고되는 린트에 상한선(cap)을 설정하는 것도 지원해요.

린트 이유 (Lint reasons)

모든 린트 속성은 추가적인 reason 매개변수를 지원해서, 특정 속성을 추가한 이유에 대한 맥락을 제공해요. 이 reason은 린트가 정의된 레벨로 내보내지면 린트 메시지의 일부로 표시돼요.

#![allow(unused)]
fn main() {
// `keyword_idents`는 기본적으로 허용돼요. 여기서는 에디션을 업데이트할 때
// 식별자 마이그레이션을 피하려고 이를 deny해요.
#![deny(
    keyword_idents,
    reason = "we want to avoid these idents to be future compatible"
)]

// 이 이름은 Rust 2015 에디션에서 허용됐어요. 우리는 여전히
// future compatible을 위해 이것을 피하고 싶어요.
fn dyn() {}
}

린트를 reason과 함께 허용하는 또 다른 예시가 아래에 있어요.

#![allow(unused)]
fn main() {
use std::path::PathBuf;

pub fn get_path() -> PathBuf {
    // `allow` 속성의 `reason` 매개변수는 독자에게 문서 역할을 해요.
    #[allow(unused_mut, reason = "this is only modified on some platforms")]
    let mut file_name = PathBuf::from("git");

    #[cfg(target_os = "windows")]
    file_name.set_extension("exe");

    file_name
}
}

#[expect] 속성

#[expect(C)] 속성은 린트 C에 대한 린트 기대(lint expectation) 를 만들어요. 같은 위치의 #[warn(C)] 속성이 린트 발생을 가져올 경우 기대가 채워져요. 린트 C가 내보내지지 않아 기대가 채워지지 않으면, 속성에서 unfulfilled_lint_expectations 린트가 내보내져요.

fn main() {
    // 이 `#[expect]` 속성은 `unused_variables` 린트가 다음 문장에 의해
    // 내보내질 것이라는 린트 기대를 만들어요. 이 기대는 채워지지 않는데,
    // `question` 변수가 `println!` 매크로에 사용되기 때문이에요.
    // 따라서 `unfulfilled_lint_expectations` 린트가 속성에서 내보내져요.
    #[expect(unused_variables)]
    let question = "who lives in a pineapple under the sea?";
    println!("{question}");

    // 이 `#[expect]` 속성은 채워질 린트 기대를 만들어요. `answer` 변수가
    // 결코 사용되지 않기 때문이에요. 보통 내보내질 `unused_variables` 린트는
    // 억제돼요. 문장이나 속성에 대해 경고가 발행되지 않아요.
    #[expect(unused_variables)]
    let answer = "SpongeBob SquarePants!";
}

린트 기대는 expect 속성이 억제한 린트 발생에 의해서만 채워져요. 스코프에서 allowwarn 같은 다른 레벨 속성으로 린트 레벨을 수정하면 린트 발생이 그에 따라 처리되고 기대는 채워지지 않은 채 남아요.

#![allow(unused)]
fn main() {
#[expect(unused_variables)]
fn select_song() {
    // 이 코드는 `warn` 속성이 정의한 warn 레벨로 `unused_variables` 린트를
    // 내보내요. 이는 함수 위의 기대를 채우지 않아요.
    #[warn(unused_variables)]
    let song_name = "Crab Rave";

    // `allow` 속성은 린트 발생을 억제해요. 이는 함수 위의 `expect` 속성이
    // 아니라 `allow` 속성에 의해 억제됐으므로 기대를 채우지 않아요.
    #[allow(unused_variables)]
    let song_creator = "Noisestorm";

    // 이 `expect` 속성은 변수에서 `unused_variables` 린트 발생을 억제해요.
    // 함수 위의 `expect` 속성은 여전히 채워지지 않아요. 이 린트 발생이 지역
    // expect 속성에 의해 억제됐기 때문이에요.
    #[expect(unused_variables)]
    let song_version = "Monstercat Release";
}
}

expect 속성이 여러 린트를 포함하면 각각이 별도로 기대돼요. 린트 그룹의 경우 그룹 안의 하나의 린트만 내보내져도 충분해요.

#![allow(unused)]
fn main() {
// 이 기대는 함수 안의 미사용 값에 의해 채워져요. 내보내진 `unused_variables`
// 린트가 `unused` 린트 그룹 안에 있기 때문이에요.
#[expect(unused)]
pub fn thoughts() {
    let unused = "I'm running out of examples";
}

pub fn another_example() {
    // 이 속성은 두 개의 린트 기대를 만들어요. `unused_mut` 린트가 억제되고
    // 그로써 첫 번째 기대를 채워요. `unused_variables`는 변수가 사용되므로
    // 내보내지지 않을 거예요. 따라서 그 기대는 채워지지 않고 경고가 내보내져요.
    #[expect(unused_mut, unused_variables)]
    let mut link = "https://www.rust-lang.org/";

    println!("Welcome to our community: {link}");
}
}

참고: #[expect(unfulfilled_lint_expectations)]의 동작은 항상 unfulfilled_lint_expectations 린트를 생성하도록 현재 정의돼 있어요.

린트 그룹

린트는 이름 있는 그룹으로 구성될 수 있어서, 관련 린트들의 레벨을 함께 조정할 수 있어요. 이름 있는 그룹을 쓰는 것은 그 그룹 안의 린트를 나열하는 것과 동등해요.

#![allow(unused)]
fn main() {
// 이 코드는 "unused" 그룹의 모든 린트를 허용해요.
#[allow(unused)]
// 이 코드는 "unused" 그룹의 "unused_must_use" 린트를 deny로 덮어써요.
#[deny(unused_must_use)]
fn example() {
    // "unused_variables" 린트가 "unused" 그룹에 있으므로 경고를 만들지 않아요.
    let x = 1;
    // 결과가 사용되지 않고 "unused_must_use"가 "deny"로 표시됐으므로
    // 이 코드는 에러를 만들어요.
    std::fs::remove_file("some_file"); // ERROR: unused `Result` that must be used
}
}

"warnings" 라는 특별한 그룹이 있어요. 이 그룹은 "warn" 레벨의 모든 린트를 포함해요. "warnings" 그룹은 속성 순서를 무시하고, 해당 개체 내에서 그렇지 않으면 경고할 모든 린트에 적용돼요.

#![allow(unused)]
fn main() {
unsafe fn an_unsafe_fn() {}
// 이 두 속성의 순서는 중요하지 않아요.
#[deny(warnings)]
// unsafe_code 린트는 보통 기본적으로 "allow"예요.
#[warn(unsafe_code)]
fn example_err() {
    // `unsafe_code` 경고가 "deny"로 승격됐으므로 이 코드는 에러예요.
    unsafe { an_unsafe_fn() } // ERROR: use of `unsafe` block
}
}

도구 린트 속성

도구 린트(tool lint) 는 스코프 있는 린트를 사용해서 특정 도구의 린트를 allow, warn, deny, forbid할 수 있게 해줘요.

도구 린트는 관련 도구가 활성일 때만 검사돼요. allow 같은 린트 속성이 존재하지 않는 도구 린트를 참조하면, 그 도구를 사용할 때까지 컴파일러는 존재하지 않는 린트에 대해 경고하지 않아요.

그 외에는 일반 린트 속성과 똑같이 작동해요.

// 전체 `pedantic` clippy 린트 그룹을 warn으로 설정
#![warn(clippy::pedantic)]
// `filter_map` clippy 린트의 경고를 침묵
#![allow(clippy::filter_map)]

fn main() {
    // ...
}

// 이 함수에 대해서만 `cmp_nan` clippy 린트를 침묵
#[allow(clippy::cmp_nan)]
fn foo() {
    // ...
}

rustc는 현재 "clippy"와 "rustdoc"의 도구 린트를 인식해요.

deprecated 속성

deprecated 속성은 항목을 더 이상 사용되지 않음(deprecated) 으로 표시해요. rustc는 #[deprecated] 항목의 사용에 대해 경고를 발행해요. rustdoc은 (가능하면) since 버전과 note를 포함해 항목 사용 중단을 보여줘요.

deprecated 속성은 여러 형태가 있어요.

  • deprecated — 일반 메시지를 발행해요.
  • deprecated = "message" — 주어진 문자열을 사용 중단 메시지에 포함해요.
  • MetaListNameValueStr 구문에 두 개의 선택적 필드:
    • since — 항목이 사용 중단된 버전 번호를 지정해요. rustc는 현재 이 문자열을 해석하지 않지만, Clippy 같은 외부 도구는 값의 유효성을 검사할 수 있어요.
    • note — 사용 중단 메시지에 포함할 문자열을 지정해요. 보통 사용 중단 이유와 권장 대안에 대한 설명을 제공하는 데 쓰여요.

deprecated 속성은 어떤 항목, 트레잇 항목, enum variant, 구조체 필드, extern 블록 항목, 매크로 정의에도 적용할 수 있어요. 트레잇 구현 항목에는 적용할 수 없어요. 모듈이나 구현처럼 다른 항목을 포함하는 항목에 적용하면 모든 자식 항목이 그 사용 중단 속성을 상속해요.

예시를 볼게요.

#![allow(unused)]
fn main() {
#[deprecated(since = "5.2.0", note = "foo was rarely used. Users should instead use bar")]
pub fn foo() {}

pub fn bar() {}
}

동기와 더 많은 세부 사항은 RFC에 있어요.

must_use 속성

must_use 속성은 사용해야 하는 값임을 표시해요.

must_use 속성은 MetaWord와 MetaNameValueStr 구문을 사용해요.

#![allow(unused)]
fn main() {
#[must_use]
fn use_me1() -> u8 { 0 }

#[must_use = "explanation of why it should be used"]
fn use_me2() -> u8 { 0 }
}

must_use 속성은 다음에 적용될 수 있어요: 구조체, 열거형, union, 함수, 트레잇.

  • rustc는 다른 위치의 사용을 무시하지만, 이에 대해 린트해요. 이건 미래에 에러가 될 수도 있어요.
  • must_use 속성은 항목에 한 번만 사용할 수 있어요. rustc는 첫 번째 이후의 사용에 대해 린트해요(미래에 에러가 될 수도 있어요).
  • must_use 속성은 MetaNameValueStr 구문으로 메시지를 포함할 수 있어요. 예: #[must_use = "example message"]. 이 메시지는 린트의 일부로 내보내질 수 있어요.

속성이 구조체, 열거형, union에 적용되면, 표현식 문장의 표현식이 그 타입을 가질 때 unused_must_use 린트가 발동해요.

#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
#[must_use]
struct MustUse();
MustUse(); // ERROR: Unused value that must be used.
}

위 규칙의 예외로, E가 uninhabited일 때 Result<(), E>, 그리고 B가 uninhabited일 때 ControlFlow<B, ()> 에서는 린트가 발동하지 않아요. 외부 크레이트의 #[non_exhaustive] 타입은 이 목적에서 uninhabited로 간주되지 않아요. 미래에 생성자를 추가할 수 있기 때문이에요.

#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
use core::ops::ControlFlow;
enum Empty {}
fn f1() -> Result<(), Empty> { Ok(()) }
f1(); // OK: `Empty`는 uninhabited예요.
fn f2() -> ControlFlow<Empty, ()> { ControlFlow::Continue(()) }
f2(); // OK: `Empty`는 uninhabited예요.
}

표현식 문장의 표현식이 호출 표현식 또는 메서드 호출 표현식이고 그 함수 피연산자가 속성이 적용된 함수라면 unused_must_use 린트가 발동해요.

#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
#[must_use]
fn f() {}
f(); // ERROR: Unused return value that must be used.
}

표현식 문장의 표현식이 호출/메서드 호출 표현식이고, 그 함수 피연산자가 impl Trait 또는 dyn Trait 타입을 반환하는 함수로, 바운드에 있는 트레잇 중 하나 이상이 속성으로 표시된 경우 unused_must_use 린트가 발동해요.

#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
#[must_use]
trait Tr {}
impl Tr for () {}
fn f() -> impl Tr {}
f(); // ERROR: Unused implementor that must be used.
}

속성이 트레잇 선언의 함수에 적용되면, 호출/메서드 호출 표현식의 함수 피연산자가 그 함수의 구현일 때 must_use.fn 규칙도 적용돼요.

#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
trait Tr {
    #[must_use]
    fn use_me(&self);
}

impl Tr for () {
    fn use_me(&self) {}
}

().use_me(); // ERROR: Unused return value that must be used.
}
#![allow(unused)]
fn main() {
#![deny(unused_must_use)]
trait Tr {
    #[must_use]
    fn use_me(&self);
}

impl Tr for () {
    fn use_me(&self) {}
}

<() as Tr>::use_me(&());
//          ^^^^^^^^^^^ ERROR: Unused return value that must be used.
}

must_use.type, must_use.fn, must_use.trait, must_use.trait-function을 검사할 때, 린트는 블록 표현식(unsafe 블록과 라벨 있는 블록 표현식 포함)을 통해 각각의 꼬리 표현식까지 들여다봐요. 이는 중첩된 블록 표현식에 대해 재귀적으로 적용돼요.

#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
#[must_use]
fn f() {}

{ f() };        // ERROR: The lint looks through block expressions.
unsafe { f() }; // ERROR: The lint looks through `unsafe` blocks.
{ { f() } };    // ERROR: The lint looks through nested blocks.
}

트레잇 구현의 함수에 사용되면 이 속성은 아무것도 하지 않아요.

#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
trait Tr {
    fn f(&self);
}

impl Tr for () {
    #[must_use] // 이건 아무 효과가 없어요.
    fn f(&self) {}
}

().f(); // OK.
}

rustc는 트레잇 구현의 함수 사용에 대해 린트해요(미래에 에러가 될 수도 있어요).

#[must_use] 함수의 결과를 특정 표현식으로 감싸면 fn 기반 검사를 억제할 수 있어요. 표현식 문장의 표현식이 #[must_use] 함수에 대한 호출/메서드 호출 표현식이 아니기 때문이에요. 전체 표현식의 타입이 #[must_use]이면 타입 기반 검사는 여전히 적용돼요.

#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
#[must_use]
fn f() {}

// 표현식 문장의 표현식이 `#[must_use]` 함수에 대한 호출이 아니므로,
// 이 중 어느 것에도 fn 기반 검사가 발동하지 않아요.
(f(),);                    // 표현식은 호출이 아니라 튜플이에요.
Some(f());                 // 호출된 `Some`은 `#[must_use]`가 아니에요.
if true { f() } else {};   // 표현식은 호출이 아니라 `if`예요.
match true {               // 표현식은 호출이 아니라 `match`예요.
    _ => f()
};
}
#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
#[must_use]
struct MustUse;
fn g() -> MustUse { MustUse }

// `if` 표현식이 호출이 아니지만, 표현식의 타입이 `#[must_use]` 속성이 있는
// `MustUse`이므로 타입 기반 검사가 발동해요.
if true { g() } else { MustUse }; // ERROR: Must be used.
}

반드시 사용해야 하는 값을 의도적으로 버릴 때 _ 패턴의 let 문이나 분해 할당(destructuring assignment)을 쓰는 것이 관용적이에요.

#![allow(unused)]
#![deny(unused_must_use)]
fn main() {
#[must_use]
fn f() {}
let _ = f(); // OK.
_ = f(); // OK.
}

diagnostic 도구 속성 네임스페이스

#[diagnostic] 속성 네임스페이스는 컴파일 타임 에러 메시지에 영향을 주는 속성들의 집합이에요. 이 속성들이 제공하는 힌트는 사용된다는 보장이 없어요.

이 네임스페이스의 알 수 없는 속성은 받아들여지지만, 사용되지 않는 속성에 대해 경고를 낼 수 있어요. 추가로, 알려진 속성에 대한 잘못된 입력은 보통 경고가 돼요(자세한 내용은 각 속성 정의 참조). 이는 미래에 속성을 추가하거나 버리고 입력을 바꿀 수 있게 해서, 의미 없는 속성이나 옵션을 계속 동작하게 할 필요 없이 변경을 허용하려는 의도예요.

diagnostic::on_unimplemented 속성

#[diagnostic::on_unimplemented] 속성은 트레잇이 요구되지만 특정 타입에 구현되지 않은 시나리오에서 보통 생성되는 에러 메시지를 보완하라는 컴파일러에 대한 힌트예요.

이 속성은 트레잇 선언에 있어야 해요 (다른 위치에 있어도 에러는 아니에요).

이 속성은 입력을 지정하기 위해 MetaListNameValueStr 구문을 사용해요. 다만 속성에 대한 잘못된 형태의 입력은, 앞으로 및 뒤로 호환성을 제공하기 위해 에러로 간주되지 않아요.

다음 키들은 주어진 의미를 가져요.

  • message — 최상위 에러 메시지의 텍스트.
  • label — 에러 메시지에서 깨진 코드 안에 인라인으로 표시되는 라벨의 텍스트.
  • note — 추가적인 주석을 제공해요. note 옵션은 여러 번 나타날 수 있고, 그 결과 여러 note 메시지가 내보내져요.

다른 옵션 중 하나라도 여러 번 나타나면 관련 옵션의 첫 번째 발생이 실제로 사용되는 값을 지정해요. 이후의 발생은 경고를 만들어요. 알 수 없는 키에 대해서는 경고가 생성돼요.

세 옵션 모두 인자로 문자열을 받고, std::fmt 문자열과 같은 형식으로 해석돼요. 주어진 이름 있는 매개변수를 가진 형식 매개변수는 다음 텍스트로 대체돼요.

  • {Self} — 트레잇을 구현하는 타입의 이름.
  • { GenericParameterName } — 주어진 제네릭 매개변수에 대한 제네릭 인자의 타입 이름.

다른 형식 매개변수는 경고를 생성하지만, 그 외에는 문자열에 그대로 포함돼요. 잘못된 형식 문자열은 경고를 낼 수 있지만 그 외에는 허용되며, 의도한 대로 표시되지 않을 수 있어요. 형식 지정자(specifier)는 경고를 낼 수 있지만 그 외에는 무시돼요.

이 예시에서:

#[diagnostic::on_unimplemented(
    message = "My Message for `ImportantTrait<{A}>` implemented for `{Self}`",
    label = "My Label",
    note = "Note 1",
    note = "Note 2"
)]
trait ImportantTrait<A> {}

fn use_my_trait(_: impl ImportantTrait<i32>) {}

fn main() {
    use_my_trait(String::new());
}

컴파일러는 다음과 같은 에러 메시지를 생성할 수 있어요.

error[E0277]: My Message for `ImportantTrait<i32>` implemented for `String`
  --> src/main.rs:14:18
   |
14 |     use_my_trait(String::new());
   |     ------------ ^^^^^^^^^^^^^ My Label
   |     |
   |     required by a bound introduced by this call
   |
   = help: the trait `ImportantTrait<i32>` is not implemented for `String`
   = note: Note 1
   = note: Note 2

diagnostic::do_not_recommend 속성

#[diagnostic::do_not_recommend] 속성은 진단 메시지의 일부로 주석이 달린 트레잇 구현을 표시하지 말도록 컴파일러에 알리는 힌트예요.

추천을 억제하는 것은, 그 추천이 보통 프로그래머에게 유용하지 않을 것임을 알 때 유용해요. 이것은 종종 광범위한 blanket impl에서 발생해요. 추천이 프로그래머를 잘못된 길로 보낼 수도 있고, 트레잇 구현이 노출하고 싶지 않은 내부 세부 사항일 수도 있으며, 바운드가 프로그래머에 의해 만족될 수 없을 수도 있어요.

예를 들어, 타입이 요구된 트레잇을 구현하지 않는다는 에러 메시지에서 컴파일러는 트레잇 구현의 특정 바운드만 아니라면 요구 사항을 만족시킬 트레잇 구현을 찾을 수 있어요. 컴파일러는 사용자에게 impl이 있다고 말할 수 있지만, 문제는 그 트레잇 구현의 바운드예요. #[diagnostic::do_not_recommend] 속성은 트레잇 구현에 대해 사용자에게 알리지 말고, 단순히 타입이 요구된 트레잇을 구현하지 않는다고만 말하도록 컴파일러에 지시하는 데 쓸 수 있어요.

이 속성은 트레잇 구현 항목에 있어야 해요 (다른 위치에 있어도 에러는 아니에요). 이 속성은 인자를 받지 않아요 (예상치 못한 인자는 에러로 간주되지 않아요).

다음 예시에는, 임의의 타입을 SQL 라이브러리에 쓰이는 Expression 타입으로 캐스팅하는 데 쓰이는 AsExpression이라는 트레잇이 있어요. check라는 메서드는 AsExpression을 받아요.

pub trait Expression {
    type SqlType;
}

pub trait AsExpression<ST> {
    type Expression: Expression<SqlType = ST>;
}

pub struct Text;
pub struct Integer;

pub struct Bound<T>(T);
pub struct SelectInt;

impl Expression for SelectInt {
    type SqlType = Integer;
}

impl<T> Expression for Bound<T> {
    type SqlType = T;
}

impl AsExpression<Integer> for i32 {
    type Expression = Bound<Integer>;
}

impl AsExpression<Text> for &'_ str {
    type Expression = Bound<Text>;
}

impl<T> Foo for T where T: Expression {}

// 이 줄의 주석을 풀면 추천을 바꿔요.
// #[diagnostic::do_not_recommend]
impl<T, ST> AsExpression<ST> for T
where
    T: Expression<SqlType = ST>,
{
    type Expression = T;
}

trait Foo: Expression + Sized {
    fn check<T>(&self, _: T) -> <T as AsExpression<<Self as Expression>::SqlType>>::Expression
    where
        T: AsExpression<Self::SqlType>,
    {
        todo!()
    }
}

fn main() {
    SelectInt.check("bar");
}

SelectInt 타입의 check 메서드는 Integer 타입을 기대해요. i32 타입으로 호출하면 AsExpression 트레잇에 의해 Integer로 변환되므로 동작해요. 하지만 문자열로 호출하면 동작하지 않고, 다음과 같은 에러가 생성될 수 있어요.

error[E0277]: the trait bound `&str: Expression` is not satisfied
  --> src/main.rs:53:15
   |
53 |     SelectInt.check("bar");
   |               ^^^^^ the trait `Expression` is not implemented for `&str`
   |
   = help: the following other types implement trait `Expression`:
             Bound<T>
             SelectInt
note: required for `&str` to implement `AsExpression<Integer>`
  --> src/main.rs:45:13
   |
45 | impl<T, ST> AsExpression<ST> for T
   |             ^^^^^^^^^^^^^^^^     ^
46 | where
47 |     T: Expression<SqlType = ST>,
   |        ------------------------ unsatisfied trait bound introduced here

AsExpression의 blanket impl에 #[diagnostic::do_not_recommend] 속성을 추가하면 메시지가 이렇게 바뀌어요.

error[E0277]: the trait bound `&str: AsExpression<Integer>` is not satisfied
  --> src/main.rs:53:15
   |
53 |     SelectInt.check("bar");
   |               ^^^^^ the trait `AsExpression<Integer>` is not implemented for `&str`
   |
   = help: the trait `AsExpression<Integer>` is not implemented for `&str`
           but trait `AsExpression<Text>` is implemented for it
   = help: for that trait implementation, expected `Text`, found `Integer`

첫 번째 에러 메시지는 &strExpression의 관계에 대한 다소 혼란스러운 메시지와 blanket impl의 사용되지 않은 트레잇 바운드를 포함해요. #[diagnostic::do_not_recommend]를 추가하면 더 이상 blanket impl을 추천으로 고려하지 않아요. 메시지는 조금 더 명확해져서, 문자열이 Integer로 변환될 수 없음을 나타내요.

더 알아보기 (Learn more)