Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 68 additions & 40 deletions src/librustdoc/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use rustc_session::{EarlyDiagCtxt, getopts};
use rustc_span::edition::Edition;
use rustc_span::{FileName, RemapPathScopeComponents};
use rustc_target::spec::TargetTuple;
use smallvec::SmallVec;

use crate::core::new_dcx;
use crate::externalfiles::ExternalHtml;
Expand Down Expand Up @@ -293,7 +294,7 @@ pub(crate) struct RenderOptions {
/// Note: this field is duplicated in `Options` because it's useful to have
/// it in both places.
pub(crate) unstable_features: rustc_feature::UnstableFeatures,
pub(crate) emit: Vec<EmitType>,
pub(crate) emit: SmallVec<[EmitType; 2]>,
/// If `true`, HTML source pages will generate links for items to their definition.
pub(crate) generate_link_to_definition: bool,
/// Set of function-call locations to include as examples
Expand Down Expand Up @@ -327,9 +328,22 @@ pub(crate) enum ModuleSorting {
pub(crate) enum EmitType {
HtmlStaticFiles,
HtmlNonStaticFiles,
// not explicitly nameable by the user for now
JsonFiles,
DepInfo(Option<OutFileName>),
}

impl fmt::Display for EmitType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::HtmlStaticFiles => "html-static-files",
Self::HtmlNonStaticFiles => "html-non-static-files",
Self::JsonFiles => "json-files",
Self::DepInfo(_) => "dep-info",
})
}
}

impl FromStr for EmitType {
type Err = ();

Expand All @@ -352,17 +366,11 @@ impl FromStr for EmitType {
}

impl RenderOptions {
pub(crate) fn should_emit_crate(&self) -> bool {
self.emit.is_empty() || self.emit.contains(&EmitType::HtmlNonStaticFiles)
}

pub(crate) fn dep_info(&self) -> Option<Option<&OutFileName>> {
for emit in &self.emit {
if let EmitType::DepInfo(file) = emit {
return Some(file.as_ref());
}
}
None
self.emit.iter().find_map(|emit| match emit {
EmitType::DepInfo(file) => Some(file.as_ref()),
_ => None,
})
}
}

Expand Down Expand Up @@ -469,26 +477,6 @@ impl Options {

let should_test = matches.opt_present("test");

let mut emit = FxIndexMap::<_, EmitType>::default();
for list in matches.opt_strs("emit") {
if should_test {
dcx.fatal("the `--test` flag and the `--emit` flag are not supported together");
}
for kind in list.split(',') {
match kind.parse() {
Ok(kind) => {
// De-duplicate emit types and the last wins.
// Only one instance for each type is allowed
// regardless the actual data it carries.
// This matches rustc's `--emit` behavior.
emit.insert(std::mem::discriminant(&kind), kind);
}
Err(()) => dcx.fatal(format!("unrecognized emission type: {kind}")),
}
}
}
let emit = emit.into_values().collect::<Vec<_>>();

let show_coverage = matches.opt_present("show-coverage");
let output_format_s = matches.opt_str("output-format");
let output_format = match output_format_s {
Expand Down Expand Up @@ -527,15 +515,55 @@ impl Options {
}
}

if output_format == OutputFormat::Json {
if let Some(emit_flag) = emit.iter().find_map(|emit| match emit {
EmitType::HtmlStaticFiles => Some("html-static-files"),
EmitType::HtmlNonStaticFiles => Some("html-non-static-files"),
EmitType::DepInfo(_) => None,
}) {
dcx.fatal(format!(
"the `--emit={emit_flag}` flag is not supported with `--output-format=json`",
));
let mut emit = FxIndexMap::default();
for list in matches.opt_strs("emit") {
Copy link
Copy Markdown
Member Author

@fmease fmease Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated and already brought up in the stabilization PR, I'm more and more leaning towards fully ditching -w, --output-format in favor of --emit because the current setup makes my head spin (the loose proposal to repurpose--output and the one to add --print doesn't help ^^).

--emit=json-files and --emit=doctests could be wonderful. Of course, we probably want to make some emission types mutually exclusive then … which might not be in line with its "spirit" but oh well

View changes since the review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, we probably want to make some emission types mutually exclusive then

This doesn't seem obvious to me. I know there are several barriers that prevent rustdoc from emitting json and html at the same time:

  1. The output format changes the resulting Clean AST, because HTML is inlined and JSON isn't.
  2. Constructing clean::Crate mutates DocContext, so we can't just construct Clean twice.
  3. You'll want to be able to specify a different output location for the JSON than for the HTML.
  4. Some cleaning-related processes produce warnings, and we don't want duplicates.

If either of those first two barriers were fixed, wouldn't --emit=html-static-files,html-non-static-files,json-file=./target/doc-json/CRATENAME.json make sense?

Copy link
Copy Markdown
Member Author

@fmease fmease May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it would indeed make sense if it's feasible to fix these issues 👍.

Let me change my statement to the following then: "If we were to introduce --emit=json-files and ditch -wjson, we would want to make the html-* emission types incompatible with json-files for the time being until we have figured out how to make them compatible implementation-wise".

In any case, this topic isn't strictly related to this PR which is a mere internal refactoring plus a fix for an unstable feature (-wdoctest) and shouldn't block it in the slightest :)

if should_test {
dcx.fatal("the `--test` flag and the `--emit` flag are not supported together");
}
if let OutputFormat::Doctest = output_format {
dcx.fatal("the `--emit` flag is not supported with `--output-format=doctest`");
}
Comment on lines +523 to +525
Copy link
Copy Markdown
Member Author

@fmease fmease Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do like this PR I'll of course add a test for this.

View changes since the review


for typ in list.split(',') {
let Ok(typ) = typ.parse::<EmitType>() else {
dcx.fatal(format!("unrecognized emission type: {typ}"))
};

match typ {
Copy link
Copy Markdown
Member Author

@fmease fmease Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A match (output_format, typ) { … } / match (typ, output_format) { … } looked less legible in my eyes esp. due to rustfmt's decisions.

View changes since the review

EmitType::DepInfo(_) => match output_format {
OutputFormat::Json | OutputFormat::Html => {}
OutputFormat::Doctest => unreachable!(),
},
EmitType::HtmlStaticFiles | EmitType::HtmlNonStaticFiles => match output_format
{
OutputFormat::Html => {}
OutputFormat::Json => dcx.fatal(format!(
"the `--emit={typ}` flag is not supported with `--output-format=json`",
)),
OutputFormat::Doctest => unreachable!(),
},
EmitType::JsonFiles => unreachable!(),
}

// De-duplicate emit types and the last wins.
// Only one instance for each type is allowed
// regardless the actual data it carries.
// This matches rustc's `--emit` behavior.
emit.insert(std::mem::discriminant(&typ), typ);
}
}
let mut emit: SmallVec<[_; 2]> = emit.into_values().collect();
// If `--emit` is absent we'll register default emission types depending on the requested
// output format. We can safely use `is_empty` for this since `--emit=` ("truly empty")
// will have already been rejected above.
if emit.is_empty() {
Copy link
Copy Markdown
Member Author

@fmease fmease Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, I could change the earlier emit from FxIndexMap to Option<FxIndexMap> to avoid conflating Some([]) with None which would be more robust and future-proof but slightly more annoying to construct.

View changes since the review

match output_format {
OutputFormat::Json => emit.push(EmitType::JsonFiles),
OutputFormat::Html => {
emit.push(EmitType::HtmlStaticFiles);
emit.push(EmitType::HtmlNonStaticFiles);
}
OutputFormat::Doctest => {}
}
}

Expand Down
20 changes: 11 additions & 9 deletions src/librustdoc/formats/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ use rustc_data_structures::profiling::SelfProfilerRef;
use rustc_middle::ty::TyCtxt;

use crate::clean;
use crate::config::RenderOptions;
use crate::config::{EmitType, RenderOptions};
use crate::error::Error;
use crate::formats::cache::Cache;

/// Allows for different backends to rustdoc to be used with the `run_format()` function. Each
/// backend renderer has hooks for initialization, documenting an item, entering and exiting a
/// module, and cleanup/finalizing output.
pub(crate) trait FormatRenderer<'tcx>: Sized {
/// Gives a description of the renderer. Used for performance profiling.
fn descr() -> &'static str;
/// A description of the renderer. Used for performance profiling.
const DESCR: &'static str;

/// Whether to call `item` recursively for modules
/// Whether to call `item` recursively for modules.
///
/// This is true for html, and false for json. See #80664
/// See [#80664](https://github.com/rust-lang/rust/issues/80664).
const RUN_ON_MODULE: bool;

const NON_STATIC_FILE_EMIT_TYPE: EmitType;

/// This associated type is the type where the current module information is stored.
///
/// For each module, we go through their items by calling for each item:
Expand Down Expand Up @@ -109,18 +111,18 @@ pub(crate) fn run_format<
) -> Result<(), Error> {
let prof = &tcx.sess.prof;

let emit_crate = options.should_emit_crate();
let emit_non_static_files = options.emit.contains(&T::NON_STATIC_FILE_EMIT_TYPE);
let (mut format_renderer, krate) = prof
.verbose_generic_activity_with_arg("create_renderer", T::descr())
.verbose_generic_activity_with_arg("create_renderer", T::DESCR)
.run(|| init(krate, options, cache, tcx))?;

if !emit_crate {
if !emit_non_static_files {
return Ok(());
}

// Render the crate documentation
run_format_inner(&mut format_renderer, &krate.module, prof)?;

prof.verbose_generic_activity_with_arg("renderer_after_krate", T::descr())
prof.verbose_generic_activity_with_arg("renderer_after_krate", T::DESCR)
.run(|| format_renderer.after_krate())
}
13 changes: 6 additions & 7 deletions src/librustdoc/html/render/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use super::{AllTypes, LinkFromSrc, StylePath, collect_spans_and_sources, scrape_
use crate::clean::types::ExternalLocation;
use crate::clean::utils::has_doc_flag;
use crate::clean::{self, ExternalCrate};
use crate::config::{ModuleSorting, RenderOptions, ShouldMerge};
use crate::config::{EmitType, ModuleSorting, RenderOptions, ShouldMerge};
use crate::docfs::{DocFS, PathError};
use crate::error::Error;
use crate::formats::FormatRenderer;
Expand Down Expand Up @@ -481,7 +481,6 @@ impl<'tcx> Context<'tcx> {
) -> Result<(Self, clean::Crate), Error> {
// need to save a copy of the options for rendering the index page
let md_opts = options.clone();
let emit_crate = options.should_emit_crate();
let RenderOptions {
output,
external_html,
Expand All @@ -495,6 +494,7 @@ impl<'tcx> Context<'tcx> {
static_root_path,
generate_redirect_map,
show_type_layout,
emit,
generate_link_to_definition,
call_locations,
no_emit_shared,
Expand Down Expand Up @@ -605,7 +605,7 @@ impl<'tcx> Context<'tcx> {
info: ContextInfo::new(include_sources),
};

if emit_crate {
if emit.contains(&EmitType::HtmlNonStaticFiles) {
sources::render(&mut cx, &krate)?;
}

Expand All @@ -619,11 +619,10 @@ impl<'tcx> Context<'tcx> {

/// Generates the documentation for `crate` into the directory `dst`
impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
fn descr() -> &'static str {
"html"
}

const DESCR: &'static str = "html";
const RUN_ON_MODULE: bool = true;
const NON_STATIC_FILE_EMIT_TYPE: EmitType = EmitType::HtmlNonStaticFiles;

type ModuleData = ContextInfo;

fn save_module_data(&mut self) -> Self::ModuleData {
Expand Down
6 changes: 3 additions & 3 deletions src/librustdoc/html/render/write_shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ fn write_rendered_cross_crate_info(
resource_suffix: &str,
) -> Result<(), Error> {
let m = &opt.should_merge;
if opt.should_emit_crate() {
if opt.emit.contains(&EmitType::HtmlNonStaticFiles) {
if include_sources {
write_rendered_cci::<SourcesPart, _>(SourcesPart::blank, dst, crates, m)?;
}
Expand All @@ -190,7 +190,7 @@ fn write_resources(
css_file_extension: Option<&Path>,
resource_suffix: &str,
) -> Result<(), Error> {
if opt.emit.is_empty() || opt.emit.contains(&EmitType::HtmlNonStaticFiles) {
if opt.emit.contains(&EmitType::HtmlNonStaticFiles) {
// Handle added third-party themes
for entry in style_files {
let theme = entry.basename()?;
Expand Down Expand Up @@ -218,7 +218,7 @@ fn write_resources(
}
}

if opt.emit.is_empty() || opt.emit.contains(&EmitType::HtmlStaticFiles) {
if opt.emit.contains(&EmitType::HtmlStaticFiles) {
let static_dir = dst.join("static.files");
try_err!(fs::create_dir_all(&static_dir), &static_dir);

Expand Down
9 changes: 4 additions & 5 deletions src/librustdoc/json/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use tracing::{debug, trace};

use crate::clean::ItemKind;
use crate::clean::types::{ExternalCrate, ExternalLocation};
use crate::config::RenderOptions;
use crate::config::{EmitType, RenderOptions};
use crate::docfs::PathError;
use crate::error::Error;
use crate::formats::FormatRenderer;
Expand Down Expand Up @@ -132,11 +132,10 @@ impl<'tcx> JsonRenderer<'tcx> {
}

impl<'tcx> FormatRenderer<'tcx> for JsonRenderer<'tcx> {
fn descr() -> &'static str {
"json"
}

const DESCR: &'static str = "json";
const RUN_ON_MODULE: bool = false;
const NON_STATIC_FILE_EMIT_TYPE: EmitType = EmitType::JsonFiles;

type ModuleData = ();

fn save_module_data(&mut self) -> Self::ModuleData {
Expand Down
4 changes: 1 addition & 3 deletions src/librustdoc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -869,9 +869,7 @@ fn main_args(early_dcx: &mut EarlyDiagCtxt, at_args: &[String]) {
};
rustc_interface::create_and_enter_global_ctxt(compiler, krate, |tcx| {
let has_dep_info = render_options.dep_info().is_some();
if render_options.emit.contains(&EmitType::HtmlNonStaticFiles)
|| render_options.emit.is_empty()
{
if render_options.emit.contains(&EmitType::HtmlNonStaticFiles) {
markdown::render_and_write(file, render_options, edition)?;
}
if has_dep_info {
Expand Down
Loading