Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rustdoc 내부 구조

이 페이지는 rustdoc의 패스와 모드를 설명합니다. rustdoc의 개요는 “Rustdoc 개요” 장을 참조하십시오.

크레이트에서 clean까지

core.rs에는 두 가지 핵심 항목이 있습니다. 바로 rustdoc::core::DocContext struct와 rustdoc::core::run_global_ctxt 함수입니다. 후자는 rustdoc이 rustc를 호출하여, rustdoc이 작업을 넘겨받을 수 있는 시점까지 크레이트를 컴파일하는 곳입니다. 전자는 크레이트를 순회하며 문서를 수집할 때 사용하는 상태 컨테이너입니다.

크레이트 순회의 주요 과정은 clean/mod.rs에서 이름이 clean_으로 시작하는 여러 함수를 통해 이루어집니다. 각 함수는 hir 또는 ty 자료 구조를 받아 rustdoc가 사용하는 clean 구조체를 출력합니다. 예를 들어, 라이프타임을 변환하는 이 함수는 다음과 같습니다.

fn clean_lifetime<'tcx>(lifetime: &hir::Lifetime, cx: &mut DocContext<'tcx>) -> Lifetime {
    if let Some(
        rbv::ResolvedArg::EarlyBound(did)
        | rbv::ResolvedArg::LateBound(_, _, did)
        | rbv::ResolvedArg::Free(_, did),
    ) = cx.tcx.named_bound_var(lifetime.hir_id)
        && let Some(lt) = cx.args.get(&did).and_then(|arg| arg.as_lt())
    {
        return lt.clone();
    }
    Lifetime(lifetime.ident.name)
}

또한 clean/mod.rs는 이후 문서 페이지를 렌더링하는 데 사용되는 “정리된(cleaned)” 추상 구문 트리(AST)의 타입을 정의합니다. 각 타입에는 보통 rustc의 어떤 AST 또는 고수준 중간 표현(HIR) 타입을 받아 적절한 “정리된” 타입으로 변환하는 clean_* 함수가 함께 있습니다. 모듈이나 연관 항목 같은 “큰” 항목은 clean 함수에서 추가 처리를 거칠 수 있지만, 대부분의 경우 이러한 impl은 단순한 변환입니다. 이 모듈의 “진입점“은 clean::utils::krate이며, 이 함수는 run_global_ctxt에서 호출됩니다.

clean::utils::krate의 첫 번째 단계는 visit_ast::RustdocVisitor를 호출하여 모듈 트리를 중간 형태인 visit_ast::Module로 처리하는 것입니다. 이 단계에서 실제로 rustc_middle::hir::Crate를 순회하며, 다음과 같은 이름 해석의 여러 측면을 정규화합니다.

  • #[doc(inline)] 및 #[doc(no_inline)] 처리
  • 중복이나 무한한 디렉터리 트리가 생기지 않도록 import glob과 순환 처리
  • 비공개 항목의 공개 use 내보내기를 인라이닝하거나, 모듈 페이지에 “Reexport” 줄을 표시하는 것
  • 기본 항목이 숨겨져 있지만 #[doc(hidden)]이 붙은 항목을 인라이닝하는 것
  • 재내보내기로 정의되었는지 여부와 관계없이 #[macro_export]된 매크로를 크레이트 루트에 표시

이 단계 이후 clean::krate는 clean_doc_module을 호출하며, 이 함수가 실제로 HIR 항목을 정리된 AST로 변환합니다. 이 단계는 크레이트 간 인라이닝이 수행되는 단계이기도 하며, 이를 위해서는 rustc_middle 자료 구조를 정리된 AST로 변환해야 합니다.

clean/mod.rs에서 일어나는 또 다른 주요 작업은 문서 주석과 #[doc=""] 속성을 Attributes struct의 별도 필드로 수집하는 것이며, 이 구조체는 직접 작성된 문서가 있는 모든 항목에 존재합니다. 이를 통해 이후 과정에서 이 문서를 더 쉽게 수집할 수 있습니다.

이 과정의 주요 출력은 대상 크레이트에서 공개적으로 문서화할 수 있는 항목을 기술하는 Item의 트리를 가진 clean::types::Crate입니다.

주유소 빼고 다 지나가는 패스(또는: 뜨거운 감자)

다음 주요 단계로 넘어가기 전에, 정리된 AST에 대해 몇 가지 중요한 “패스“가 수행됩니다. 이러한 패스 중 일부는 lint와 보고이지만, 일부는 항목을 변경하거나 새 항목을 생성합니다.

이들은 모두 librustdoc/passes 디렉터리에 패스당 파일 하나씩 구현되어 있습니다. 기본적으로 이 모든 패스가 크레이트에 대해 실행되지만, 비공개/숨겨진 항목을 제거하는 패스는 rustdoc에 --document-private-items를 전달하여 건너뛸 수 있습니다. 이전의 AST 변환들과 달리, 이 패스들은 정리된 크레이트에 대해 실행된다는 점에 유의하십시오.

2023년 3월 기준 패스 목록은 다음과 같습니다:
  • calculate-doc-coverage는 --show-coverage 플래그에 사용되는 정보를 계산합니다.

  • check-doc-test-visibility는 doctest 가시성과 관련된 lint들을 실행합니다. 이 패스는 strip-private보다 먼저 실행되므로 run-lints와 분리되어 있어야 합니다.

  • collect-intra-doc-links는 문서 내부 링크(intra-doc links)를 해석합니다.

  • collect-trait-impls는 크레이트의 각 항목에 대한 trait impl들을 수집합니다. 예를 들어, trait를 구현하는 struct를 정의하면 이 패스는 해당 struct가 그 trait를 구현한다는 사실을 기록합니다.

  • propagate-doc-cfg는 #[doc(cfg(...))]를 하위 항목으로 전파합니다.

  • run-lints는 passes/lint에 정의된 rustdoc의 lint 중 일부를 실행합니다. 이것이 마지막으로 실행되는 패스입니다.

    • bare_urls는 링크로 만들어지지 않은 링크를 감지합니다. 예를 들어 Markdown의 Go to https://example.com/.와 같은 경우입니다. 이를 링크로 만들기 위해 Go to <https://example.com/>.처럼 링크를 꺾쇠괄호로 감싸도록 제안합니다. 이것이 rustdoc::bare_urls lint의 바탕이 되는 코드입니다.

    • check_code_block_syntax는 Rust 코드 블록 내부의 구문을 검증합니다(```rust)

    • html_tags는 문서 주석에서 유효하지 않은 HTML(닫히지 않은 <span> 등)을 감지합니다.

  • strip-hidden과 strip-private는 출력에서 모든 doc(hidden) 항목과 비공개 항목을 제거합니다. strip-private는 strip-priv-imports를 포함합니다. 기본적으로 그 목표는 공개 문서와 관련이 없는 항목을 제거하는 것입니다. --document-hidden-items가 전달되면 이 패스는 건너뜁니다.

  • strip-priv-imports는 크레이트에서 모든 비공개 임포트 문(use, extern crate)을 제거합니다. 이는 rustdoc가 공개 import를 처리할 때 항목의 문서를 모듈에 인라이닝하거나 해당 import를 포함하는 “Reexports” 섹션을 만들기 때문에 필요합니다. 이 패스는 이러한 임포트가 모두 실제로 문서와 관련이 있도록 보장합니다. 엄밀히 말하면 이 패스는 --document-private-items가 전달될 때만 실행되지만, strip-private도 같은 작업을 수행합니다.

  • strip-private는 크레이트에서 외부에서 볼 수 없는 모든 비공개 항목을 제거합니다. --document-private-items가 전달되면 이 패스는 건너뜁니다.

librustdoc/passes에는 stripper 모듈도 있지만, 이는 strip-* 패스들을 위한 유틸리티 함수 모음일 뿐 그 자체로 패스는 아닙니다.

clean에서 HTML까지

여기서부터 rustdoc의 “두 번째 단계“가 시작됩니다. 이 단계는 주로 librustdoc/formats 및 librustdoc/html 폴더에 있으며, 모든 것은 formats::renderer::run_format에서 시작합니다. 이 코드는 impl FormatRenderer인 타입을 설정하는 역할을 하며, HTML의 경우 그 타입은 Context입니다.

이 구조체에는 문서 렌더링을 진행하기 위해 run_format이 호출하는 메서드들이 들어 있으며, 다음과 같습니다:

  • init는 static.files뿐만 아니라 검색 인덱스와 src/도 생성합니다.
  • item은 항목 HTML 파일 자체를 생성합니다.
  • after_krate는 all.html과 같은 그 밖의 전역 리소스를 생성합니다.

item에서는 html/layout.rs에서 시작하여 Askama 템플릿과 수동 write!() 호출을 혼용하는 방식으로 “페이지 렌더링“이 이루어집니다. 아직 템플릿으로 전환되지 않은 부분은 일련의 std::fmt::Display 구현과 &mut std::fmt::Formatter를 주고받는 함수 안에서 처리됩니다.

항목과 문서로부터 실제로 HTML을 생성하는 부분은 html/render/print_item.rs에 정의된 print_item에서 시작하며, 이 함수는 렌더링되는 Item의 종류에 따라 여러 item_* 함수 중 하나로 분기합니다.

찾고 있는 렌더링 코드의 종류에 따라, “struct 페이지에 어떤 섹션을 출력해야 하는가“와 같은 주요 항목은 html/render/mod.rs에서, “다른 항목의 일부로 where 절을 어떻게 출력해야 하는가“와 같은 더 작은 구성 요소는 html/format.rs에서 찾을 수 있을 것입니다.

rustdoc은 직접 작성된 문서를 함께 출력해야 하는 항목을 만날 때마다 Markdown 파서와 연동하는 html/markdown.rs를 호출합니다. 이는 Markdown 문자열을 감싸고 fmt::Display를 구현하여 HTML 텍스트를 출력하는 일련의 타입으로 제공됩니다. 이 모듈은 Markdown 파서를 실행하기 전에 각주와 표 같은 특정 기능을 활성화하고 (html/highlight.rs를 통해) Rust 코드 블록에 구문 강조를 추가하는 데 특히 주의를 기울입니다. 또한 find_testable_codes가 호출하는 find_codes 함수도 있는데, 이 함수는 테스트 실행 코드가 크레이트 안의 모든 doctest를 찾을 수 있도록 Rust 코드 블록만을 골라 탐색합니다.

처음부터 끝까지 (또는: “최초의 Cell들로부터 우리에게까지 끊어지지 않은 실이 이어져 있습니다”)

rustdoc은 HTML을 생성하는 도중에도 컴파일러에 타입 정보를 직접 요청할 수 있다는 점에 유의해야 합니다. 예전에는 그렇지 않았기 때문에 rustdoc 아키텍처의 많은 부분이 그렇게 하지 않는 것을 전제로 설계되었지만, 이제는 formats::renderer::run_format에 TyCtxt가 전달됩니다. 이 함수는 HTML과 ( 2026년 5월 기준 불안정한) JSON 형식 모두의 생성을 실행하는 데 사용됩니다.

이 변경 덕분에 TyCtxt 쿼리로 쉽게 도출할 수 있는 데이터를 “clean” AST에서 제거하는 다른 변경들이 가능해졌으며, “clean“에서 필드를 제거하는 PR은 대체로 받아들여지지만(clean은 사실상 폐기 예정 상태입니다), 이는 rustdoc이 따라야 하는 다른 두 가지 제약 때문에 복잡해집니다.

  • 실제로 타입 검사를 통과하지 못하는 크레이트에 대해서도 문서를 생성할 수 있습니다. 이는 libstd가 지원되는 모든 운영 체제를 아우르는 단일 문서 패키지를 갖는 경우처럼, 상호 배타적인 플랫폼 구성을 모두 다루는 문서를 생성하는 데 사용됩니다. 즉, rustdoc은 HIR로부터 문서를 생성할 수 있어야 합니다.
  • 문서는 크레이트 경계를 넘어 인라인될 수 있습니다. 크레이트 메타데이터에는 HIR가 포함되지 않으므로, rustc_middle 데이터로부터 인라인된 문서를 생성할 수 있어야 합니다.

“clean” AST는 두 입력 형식 모두에 대한 공통 출력 형식 역할을 합니다. 또한 clean에는 자동 트레이트에 대한 합성 impl이나 collect-trait-impls 패스가 생성하는 블랭킷 impl처럼 HIR에 직접 대응하지 않는 데이터도 일부 있습니다.

일부 추가 데이터는 html::render::context::{Context, SharedContext}에 저장됩니다. 이 두 타입은 향후 다중 스레드 문서 생성을 대비하여 rustdoc의 데이터를 분리하는 수단이자, 단순히 코드를 정리된 상태로 유지하는 수단이기도 합니다.

  • Context는 현재 페이지의 경로, (중복된 id=""를 피하기 위해) 이미 사용된 HTML ID 목록, SharedContext에 대한 포인터 등 현재 페이지를 생성하는 데 사용되는 데이터를 저장합니다.
  • SharedContext는 tcx 포인터와 모든 타입의 목록처럼 페이지마다 달라지지 않는 데이터를 저장합니다.

그 밖의 숨은 기능

지금까지는 Rust 크레이트로부터 HTML 문서를 생성하는 과정을 설명했지만, rustdoc에는 이 밖에도 몇 가지 주요 실행 모드가 있습니다. 독립된 Markdown 파일에 대해 실행할 수도 있고, Rust 코드나 독립된 Markdown 파일의 doctest를 실행할 수도 있습니다. 전자의 경우 곧바로 html/markdown.rs로 넘어가며, 출력 HTML에 목차를 삽입하는 모드를 선택적으로 포함할 수 있습니다.

후자의 경우 rustdoc은 test.rs에서 관련 문서를 얻기 위해 비슷한 부분 컴파일을 실행하지만, 전체 정리 및 렌더링 과정을 거치는 대신 훨씬 단순한 크레이트 순회를 실행하여 직접 작성된 문서만 가져옵니다. 앞서 언급한 html/markdown.rs의 “find_testable_code“와 함께, 실행할 테스트 모음을 빌드한 후 테스트 러너에 넘깁니다. test.rs에서 주목할 만한 위치 중 하나는 make_test 함수로, 직접 작성된 doctest가 실행 가능한 형태로 변환되는 곳입니다.

make_test에 관한 추가 자료는 여기에서 찾을 수 있습니다.

로컬에서 테스트하기

생성된 HTML 문서의 일부 기능은 페이지 간에 로컬 스토리지를 사용해야 할 수 있는데, 이는 HTTP 서버 없이는 제대로 작동하지 않습니다. 이러한 기능을 로컬에서 테스트하려면 다음과 같이 로컬 HTTP 서버를 실행할 수 있습니다:

$ ./x doc library
# The documentation has been generated into `build/[YOUR ARCH]/doc`.
$ python3 -m http.server -d build/[YOUR ARCH]/doc

이제 문서가 인터넷에 호스팅되어 있을 때와 똑같이 문서를 탐색할 수 있습니다. 예를 들어 std의 url은 rust/std/입니다.

함께 보기