진단 구조체와 하위 진단 구조체
rustc에는 진단을 만드는 데 사용할 수 있는 두 가지 진단 트레이트인 Diagnostic과 Subdiagnostic이 있습니다.
간단한 진단에는 #[derive(Diagnostic)]과 같은 파생 impl을 사용할 수 있습니다. 이 방식은 추가 하위 진단을 붙일지 결정하는 데 로직이 많이 필요하지 않은 간단한 진단에만 적합합니다.
하위 진단을 조건에 따라 추가하거나, 렌더링 로직을 사용자 정의하거나, 런타임에 메시지를 선택하는 것처럼 진단에 더 복잡하거나 동적인 동작이 필요한 경우에는 해당 트레이트(Diagnostic 또는 Subdiagnostic)를 직접 구현해야 합니다. 이 방식은 더 큰 유연성을 제공하며, 단순하고 정적인 구조를 넘어서는 진단에 권장됩니다.
진단은 여러 언어로 번역될 수 있습니다.
#[derive(Diagnostic)]
아래에 나온 “field already declared” 진단의 정의를 살펴보십시오.
#[derive(Diagnostic)]
#[diag("field `{$field_name}` is already declared", code = E0124)]
pub struct FieldAlreadyDeclared {
pub field_name: Ident,
#[primary_span]
#[label("field already declared")]
pub span: Span,
#[label("`{$field_name}` first declared here")]
pub prev_span: Span,
}
Diagnostic은 구조체와 열거형에서만 derive할 수 있습니다. 구조체에서 타입에 붙이는 속성은 열거형에서는 각 배리언트에 붙입니다(그 반대도 마찬가지입니다). 각 Diagnostic에는 구조체 또는 각 열거형 배리언트에 적용된 #[diag(...)] 속성이 하나 있어야 합니다.
오류에 오류 코드(예: “E0624”)가 있으면 code 하위 속성을 사용해 지정할 수 있습니다. code를 반드시 지정해야 하는 것은 아니지만, Diag를 사용하는 진단을 Diagnostic을 사용하도록 옮기는 경우 기존에 코드가 있었다면 그대로 유지해야 합니다.
#[diag(..)]는 첫 번째 위치 인자로 메시지를 제공해야 합니다. 메시지는 영어로 작성하지만, 사용자가 요청한 로캘로 번역될 수 있습니다. 번역 가능한 오류 메시지를 작성하는 방법과 생성되는 방식에 대해 더 알아보려면 번역 문서를 참고하십시오.
어노테이션이 없는 Diagnostic의 모든 필드는 위 예제의 field_name처럼 Fluent 메시지에서 변수로 사용할 수 있습니다.
(타입이 Span인) 필드에 #[primary_span] 속성을 사용하면 해당 필드가 진단의 주 메시지가 표시될 주 스팬임을 나타냅니다.
진단은 주 메시지만으로 이루어지지 않으며, 레이블, 노트, 도움말 메시지, 제안을 포함하는 경우가 많습니다. 이들은 모두 Diagnostic에 지정할 수도 있습니다.
#[label], #[help], #[warning], #[note]는 모두 타입이 Span인 필드에 적용할 수 있습니다. 이 속성 중 하나를 적용하면 해당 Span을 가진 하위 진단이 만들어집니다. 이 속성들은 진단 메시지를 인자로 받습니다.
다른 타입은 Diagnostic derive에서 사용될 때 다음과 같이 특별하게 동작합니다:
Option<T>에 적용된 속성은 옵션이Some(..)일 때만 하위 진단을 내보냅니다.Vec<T>에 적용된 속성은 벡터의 각 요소마다 반복됩니다.
#[help], #[warning], #[note]는 구조체 자체에도 적용할 수 있습니다. 이 경우 하위 진단에 Span이 없다는 점만 빼면 필드에 적용했을 때와 똑같이 동작합니다. 이 속성들은 같은 효과를 내도록 () 타입의 필드에도 적용할 수 있으며, Option 타입과 함께 사용하면 선택적인 #[note]/#[help]/#[warning] 하위 진단을 나타낼 수 있습니다.
제안은 다음 네 가지 필드 속성 중 하나를 사용하여 내보낼 수 있습니다:
#[suggestion("message", code = "...", applicability = "...")]#[suggestion_hidden("message", code = "...", applicability = "...")]#[suggestion_short("message", code = "...", applicability = "...")]#[suggestion_verbose("message", code = "...", applicability = "...")]
제안은 Span 필드나 (Span, MachineApplicability) 필드 중 하나에 적용해야 합니다. 다른 필드 속성과 마찬가지로 사용자에게 표시될 메시지를 제공해야 합니다. code는 대체 코드로 제안할 코드를 지정하며, 형식 문자열입니다(예: {field_name}은 구조체의 field_name 필드 값으로 대체됩니다). applicability는 속성에서 적용 가능성을 지정하는 데 사용할 수 있지만, 필드의 타입에 Applicability가 포함된 경우에는 사용할 수 없습니다.
최종적으로 Diagnostic derive는 다음과 같은 Diagnostic 구현을 생성합니다:
impl<'a, G: EmissionGuarantee> Diagnostic<'a> for FieldAlreadyDeclared {
fn into_diag(self, dcx: &'a DiagCtxt, level: Level) -> Diag<'a, G> {
let mut diag = Diag::new(dcx, level, "field `{$field_name}` is already declared");
diag.set_span(self.span);
diag.span_label(
self.span,
"field already declared"
);
diag.span_label(
self.prev_span,
"`{$field_name}` first declared here"
);
diag
}
}
이제 진단을 정의했으니, 이를 어떻게 사용하겠습니까? 아주 간단합니다. 구조체의 인스턴스를 만들어 emit_err(또는 emit_warning)에 전달하기만 하면 됩니다:
tcx.dcx().emit_err(FieldAlreadyDeclared {
field_name: f.ident,
span: f.span,
prev_span,
});
#[derive(Diagnostic)] 참조
#[derive(Diagnostic)]는 다음 속성을 지원합니다:
#[diag("message", code = "...")]- 구조체 또는 열거형 배리언트에 적용됩니다.
- 필수
- 진단과 연결될 텍스트와 오류 코드를 정의합니다.
- 메시지(필수)
- 사용자에게 표시될 진단 메시지입니다.
- 번역 문서를 참조하십시오.
code = "..."(선택)- 오류 코드를 지정합니다.
#[note("message")](선택)- 구조체 또는 타입이
Span,Option<()>,bool,()인 구조체 필드에 적용됩니다. - note 하위 진단을 추가합니다.
- 값은 note의 메시지입니다.
Span필드에 적용하면 스팬이 지정된 note를 생성합니다.
- 구조체 또는 타입이
#[help("message")](선택)- 구조체 또는 타입이
Span,Option<()>,bool,()인 구조체 필드에 적용됩니다. - help 하위 진단을 추가합니다.
- 값은 help 메시지입니다.
Span필드에 적용하면 스팬이 지정된 help를 생성합니다.
- 구조체 또는 타입이
#[label("message")](선택)Span필드에 적용됩니다.- label 하위 진단을 추가합니다.
- 값은 label의 메시지입니다.
#[warning("message")](선택)Span,Option<()>,bool또는()타입의 구조체나 구조체 필드에 적용됩니다.- 경고 하위 진단을 추가합니다.
- 값은 경고 메시지입니다.
#[suggestion("message", code = "...", applicability = "...", style = "...")](선택 사항)(Span, MachineApplicability)또는Span필드에 적용됩니다.- 제안 하위 진단을 추가합니다.
- 메시지 (필수)
- 값은 사용자에게 표시될 제안 메시지입니다.
- 번역 문서를 참고하십시오.
code = "..."/code("...", ...)(필수)- 대체용으로 제안할 코드를 나타내는 하나 이상의 형식 문자열입니다. 값이 여러 개이면 가능한 대체안이 여러 개임을 의미합니다.
applicability = "..."(선택 사항)machine-applicable,maybe-incorrect,has-placeholders,unspecified중 하나여야 하는 문자열입니다.
style = "..."(선택 사항)- 값은 제안의 스타일입니다.
normal,short,hidden,verbose,tool-only중 하나여야 하는 문자열입니다.
#[subdiagnostic]Subdiagnostic을 구현하는 타입(#[derive(Subdiagnostic)]로 생성)에 적용됩니다.- 하위 진단 구조체가 나타내는 하위 진단을 추가합니다.
#[primary_span](선택 사항)- _
Subdiagnostic의Span필드에 적용됩니다. - 진단의 주 스팬을 나타냅니다.
- _
#[derive(Subdiagnostic)]
컴파일러에서는 특정 하위 진단이 적용 가능한 경우 이를 오류에 조건부로 추가하는 함수를 작성하는 일이 흔합니다. 전체 진단은 진단 구조체로 나타낼 수 없더라도, 이러한 하위 진단은 진단 구조체로 나타낼 수 있는 경우가 많습니다. 이러한 상황에서는 Subdiagnostic derive를 사용하여 부분 진단(예: note, label, help 또는 suggestion)을 구조체로 나타낼 수 있습니다.
아래에 나온 “expected return type” 레이블의 정의를 살펴봅시다.
#![allow(unused)]
fn main() {
#[derive(Subdiagnostic)]
pub enum ExpectedReturnTypeLabel<'tcx> {
#[label("expected `()` because of default return type")]
Unit {
#[primary_span]
span: Span,
},
#[label("expected `{$expected}` because of return type")]
Other {
#[primary_span]
span: Span,
expected: Ty<'tcx>,
},
}
}
Diagnostic과 마찬가지로 Subdiagnostic도 구조체나 열거형에 대해 derive할 수 있습니다. 구조체에서 타입에 지정하는 속성은 열거형에서는 각 변형에 지정합니다(그 반대도 마찬가지입니다). 각 Subdiagnostic에는 구조체 또는 각 변형에 다음 중 하나의 속성이 적용되어야 합니다.
- 레이블을 정의하는
#[label(..)] - 노트를 정의하는
#[note(..)] - 도움말을 정의하는
#[help(..)] - 경고를 정의하는
#[warning(..)] - 제안을 정의하는
#[suggestion(..)]
위의 모든 항목은 첫 번째 위치 인자로 진단 메시지를 제공해야 합니다. 번역 가능한 오류 메시지가 생성되는 방식에 대해 자세히 알아보려면 번역 문서를 참고하십시오.
(Span 타입의) 필드에 #[primary_span] 속성을 사용하면 해당 필드가 하위 진단의 기본 스팬임을 나타냅니다. 기본 스팬은 스팬 없이 존재할 수 없는 레이블이나 제안에만 필요합니다.
타입/변형의 필드 중 어노테이션이 없는 모든 필드는 Fluent 메시지에서 변수로 사용할 수 있습니다.
Diagnostic과 마찬가지로 Subdiagnostic도 Option<T> 및 Vec<T> 필드를 지원합니다.
제안은 타입/변형에 다음 네 가지 속성 중 하나를 사용하여 내보낼 수 있습니다:
#[suggestion("...", code = "...", applicability = "...")]#[suggestion_hidden("...", code = "...", applicability = "...")]#[suggestion_short("...", code = "...", applicability = "...")]#[suggestion_verbose("...", code = "...", applicability = "...")]
제안을 사용하려면 필드에 #[primary_span]이 설정되어 있어야 하며, 제안은 다음과 같은 하위 속성을 가질 수 있습니다:
- 첫 번째 위치 인자는 사용자에게 표시될 메시지를 지정합니다.
code는 대체 코드로 제안할 코드를 지정하며, Fluent 식별자가 아닌 형식 문자열입니다(예:{field_name}은 구조체의field_name필드 값으로 대체됩니다).applicability는 속성에서 적용 가능성을 지정하는 데 사용할 수 있지만, 필드의 타입에Applicability가 포함된 경우에는 사용할 수 없습니다.
적용 가능성은 #[applicability] 속성을 사용하여 (Applicability 타입의) 필드로 지정할 수도 있습니다.
최종적으로 Subdiagnostic derive는 다음과 같은 Subdiagnostic 구현을 생성합니다:
#![allow(unused)]
fn main() {
impl<'tcx> Subdiagnostic for ExpectedReturnTypeLabel<'tcx> {
fn add_to_diag(self, diag: &mut rustc_errors::Diagnostic) {
use rustc_errors::{Applicability, IntoDiagArg};
match self {
ExpectedReturnTypeLabel::Unit { span } => {
diag.span_label(span, "expected `()` because of default return type")
}
ExpectedReturnTypeLabel::Other { span, expected } => {
diag.set_arg("expected", expected);
diag.span_label(span, "expected `{$expected}` because of return type")
}
}
}
}
}
하위 진단을 정의한 후에는 진단의 subdiagnostic 함수에 전달하거나(예시 및 예시), 진단 구조체에서 #[subdiagnostic] 어노테이션이 달린 필드에 할당하여 사용할 수 있습니다.
인자 공유와 격리
하위 진단은 정보를 렌더링하기 전에 자신의 인자(즉, 자기 구조의 특정 필드)를 Diag 구조에 추가합니다. Diag 구조는 주 진단의 인자도 저장하므로 하위 진단에서도 주 진단의 인자를 사용할 수 있습니다.
그러나 #[derive(Subdiagnostic)]을 구현하여 하위 진단을 주 진단에 추가할 때는, rust-lang/rust#142724에서 도입된 다음 규칙이 인자(즉, Fluent 메시지에서 사용되는 변수) 처리에 적용됩니다.
하위 진단 간 인자 격리: 하위 진단이 설정한 인자는 해당 하위 진단을 렌더링하는 동안에만 사용할 수 있습니다. 하위 진단이 렌더링된 후에는 그 하위 진단이 도입한 모든 인자가 주 진단의 값으로 복원됩니다. 이를 통해 여러 하위 진단이 서로의 인자 스코프를 오염시키지 않도록 보장합니다. 예를 들어 Vec<Subdiag>를 사용할 때 각 하위 진단은 같은 인자를 반복해서 추가할 수 있습니다.
하위 진단과 주 진단 간 동일 인자 재정의: 하위 진단이 주 진단에 이미 있는 인자와 같은 이름의 인자를 설정하면, 두 값이 정확히 같지 않은 한 런타임에 오류를 보고합니다. 여기에는 두 가지 이점이 있습니다:
- 이는 주 진단의 인자가 하위 진단의 속성에 나타날 수 있는 유연성을 유지합니다. 예를 들어, 하위 진단에 있는
#[suggestion("...", code = "{new_vis}")]와 같은 속성은 메인 진단 구조체의 필드인new_vis를 참조할 수 있습니다. - 이는 주 진단이나 다른 하위 진단에 필요한 인자가 실수로 덮어쓰이거나 삭제되는 것을 방지합니다.
이 규칙들은 하위 진단이 주입한 인자가 엄격하게 자신의 렌더링 범위로 한정되도록 보장합니다. 이름 충돌이 있더라도 주 진단의 인자는 하위 진단 로직의 영향을 받지 않습니다. 또한 하위 진단은 필요할 때 같은 이름을 가진 주 진단의 인자에 접근할 수 있습니다.
#[derive(Subdiagnostic)] 레퍼런스
#[derive(Subdiagnostic)]는 다음 속성을 지원합니다:
#[label("message")],#[help("message")],#[warning("message")]또는#[note("message")]- 구조체 또는 열거형 배리언트에 적용됩니다. 구조체/열거형 배리언트 속성과 상호 배타적입니다.
- 필수
- 레이블, 도움말 또는 노트를 나타내는 타입을 정의합니다.
- 메시지 (필수)
- 사용자에게 표시될 진단 메시지입니다.
- 번역 문서를 참조하십시오.
#[suggestion("message", code = "...", applicability = "...", style = "...")]- 구조체 또는 열거형 배리언트에 적용됩니다. 구조체/열거형 배리언트 속성과 상호 배타적입니다.
- 필수
- 제안을 나타내는 타입을 정의합니다.
- 메시지 (필수)
- 사용자에게 표시될 진단 메시지입니다.
- 번역 문서를 참조하십시오.
code = "..."/code("...", ...)(필수)- 대체로 제안할 코드를 나타내는 하나 이상의 형식 문자열입니다. 값이 여러 개이면 가능한 대체안이 여러 개임을 뜻합니다.
applicability = "..."(선택)- 필드의
#[applicability]와 상호 배타적입니다. - 값은 제안의 적용 가능성입니다.
- 다음 중 하나여야 하는 문자열입니다:
machine-applicablemaybe-incorrecthas-placeholdersunspecified
- 필드의
style = "..."(선택 사항)- 값은 제안의 스타일입니다.
- 다음 중 하나여야 하는 문자열입니다:
normal(기본값)shorthiddenverbosetool-only
#[multipart_suggestion("message", applicability = "...", style = "...")]- 구조체 또는 열거형 배리언트에 적용됩니다. 구조체/열거형 배리언트 속성과는 상호 배타적입니다.
- 필수
- 다중 파트 제안(multipart suggestion)을 나타낼 타입을 정의합니다.
- 메시지(필수):
#[suggestion]을 참고하십시오 applicability = "..."(선택 사항):#[suggestion]을 참고하십시오style = "..."(선택 사항):#[suggestion]을 참고하십시오
#[primary_span](레이블과 제안에는 필수, 그 외에는 선택 사항, 다중 파트 제안에는 적용되지 않음)Span필드에 적용됩니다.- 하위 진단의 주 스팬을 나타냅니다.
#[suggestion_part(code = "...")](필수, 다중 파트 제안에만 적용 가능)Span필드에 적용됩니다.- 해당 스팬이 다중 파트 제안의 한 부분임을 나타냅니다.
code = "..."(필수)- 값은 교체용으로 제안할 코드를 나타내는 형식 문자열입니다.
#[applicability](선택 사항, (단순 및 다중 파트) 제안에만 적용 가능)Applicability필드에 적용됩니다.- 제안의 적용 가능성을 나타냅니다.