Auto merge of #124611 - Urgau:rustdoc-stdin, r=GuillaumeGomez

Add `-` (stdin) support in rustdoc

This PR adds support for the special `-` input which threats the input as coming from *stdin* instead of being a filepath.

Doing this also makes `rustdoc` consistent with `rustc` and ~~every~~ other tools. Full [motivation](https://github.com/rust-lang/rust/pull/124611#issuecomment-2094234876).

Fixes https://github.com/rust-lang/rust/issues/123671
r? `@fmease`
This commit is contained in:
bors 2024-05-18 10:53:47 +00:00
commit bb97203e37
8 changed files with 86 additions and 29 deletions

View File

@ -798,6 +798,7 @@ pub enum DumpSolverProofTree {
Never,
}
#[derive(Clone)]
pub enum Input {
/// Load source code from a file.
File(PathBuf),

View File

@ -417,6 +417,12 @@ When `rustdoc` receives this flag, it will print an extra "Version (version)" in
the crate root's docs. You can use this flag to differentiate between different versions of your
library's documentation.
## `-`: load source code from the standard input
If you specify `-` as the INPUT on the command line, then `rustdoc` will read the
source code from stdin (standard input stream) until the EOF, instead of the file
system with an otherwise specified path.
## `@path`: load command-line flags from a path
If you specify `@path` on the command-line, then it will open `path` and read

View File

@ -1,6 +1,9 @@
use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::fmt;
use std::io;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
@ -9,14 +12,14 @@ use rustc_session::config::{
self, parse_crate_types_from_list, parse_externs, parse_target_triple, CrateType,
};
use rustc_session::config::{get_cmd_lint_options, nightly_options};
use rustc_session::config::{
CodegenOptions, ErrorOutputType, Externs, JsonUnusedExterns, UnstableOptions,
};
use rustc_session::config::{CodegenOptions, ErrorOutputType, Externs, Input};
use rustc_session::config::{JsonUnusedExterns, UnstableOptions};
use rustc_session::getopts;
use rustc_session::lint::Level;
use rustc_session::search_paths::SearchPath;
use rustc_session::EarlyDiagCtxt;
use rustc_span::edition::Edition;
use rustc_span::FileName;
use rustc_target::spec::TargetTriple;
use crate::core::new_dcx;
@ -60,7 +63,7 @@ impl TryFrom<&str> for OutputFormat {
pub(crate) struct Options {
// Basic options / Options passed directly to rustc
/// The crate root or Markdown file to load.
pub(crate) input: PathBuf,
pub(crate) input: Input,
/// The name of the crate being documented.
pub(crate) crate_name: Option<String>,
/// Whether or not this is a bin crate
@ -179,7 +182,7 @@ impl fmt::Debug for Options {
}
f.debug_struct("Options")
.field("input", &self.input)
.field("input", &self.input.source_name())
.field("crate_name", &self.crate_name)
.field("bin_crate", &self.bin_crate)
.field("proc_macro_crate", &self.proc_macro_crate)
@ -320,6 +323,23 @@ impl RenderOptions {
}
}
/// Create the input (string or file path)
///
/// Warning: Return an unrecoverable error in case of error!
fn make_input(early_dcx: &EarlyDiagCtxt, input: &str) -> Input {
if input == "-" {
let mut src = String::new();
if io::stdin().read_to_string(&mut src).is_err() {
// Immediately stop compilation if there was an issue reading
// the input (for example if the input stream is not UTF-8).
early_dcx.early_fatal("couldn't read from stdin, as it did not contain valid UTF-8");
}
Input::Str { name: FileName::anon_source_code(&src), input: src }
} else {
Input::File(PathBuf::from(input))
}
}
impl Options {
/// Parses the given command-line for options. If an error message or other early-return has
/// been printed, returns `Err` with the exit code.
@ -447,15 +467,16 @@ impl Options {
let (lint_opts, describe_lints, lint_cap) = get_cmd_lint_options(early_dcx, matches);
let input = PathBuf::from(if describe_lints {
let input = if describe_lints {
"" // dummy, this won't be used
} else if matches.free.is_empty() {
dcx.fatal("missing file operand");
} else if matches.free.len() > 1 {
dcx.fatal("too many file operands");
} else {
&matches.free[0]
});
match matches.free.as_slice() {
[] => dcx.fatal("missing file operand"),
[input] => input,
_ => dcx.fatal("too many file operands"),
}
};
let input = make_input(early_dcx, &input);
let externs = parse_externs(early_dcx, matches, &unstable_opts);
let extern_html_root_urls = match parse_extern_html_roots(matches) {
@ -792,8 +813,10 @@ impl Options {
}
/// Returns `true` if the file given as `self.input` is a Markdown file.
pub(crate) fn markdown_input(&self) -> bool {
self.input.extension().is_some_and(|e| e == "md" || e == "markdown")
pub(crate) fn markdown_input(&self) -> Option<&Path> {
self.input
.opt_path()
.filter(|p| matches!(p.extension(), Some(e) if e == "md" || e == "markdown"))
}
}

View File

@ -32,7 +32,7 @@ use crate::config::{Options as RustdocOptions, OutputFormat, RenderOptions};
use crate::formats::cache::Cache;
use crate::passes::{self, Condition::*};
pub(crate) use rustc_session::config::{Input, Options, UnstableOptions};
pub(crate) use rustc_session::config::{Options, UnstableOptions};
pub(crate) struct DocContext<'tcx> {
pub(crate) tcx: TyCtxt<'tcx>,
@ -204,8 +204,6 @@ pub(crate) fn create_config(
// Add the doc cfg into the doc build.
cfgs.push("doc".to_string());
let input = Input::File(input);
// By default, rustdoc ignores all lints.
// Specifically unblock lints relevant to documentation or the lint machinery itself.
let mut lints_to_show = vec![

View File

@ -93,8 +93,6 @@ pub(crate) fn run(
dcx: &rustc_errors::DiagCtxt,
options: RustdocOptions,
) -> Result<(), ErrorGuaranteed> {
let input = config::Input::File(options.input.clone());
let invalid_codeblock_attributes_name = crate::lint::INVALID_CODEBLOCK_ATTRIBUTES.name;
// See core::create_config for what's going on here.
@ -140,7 +138,7 @@ pub(crate) fn run(
opts: sessopts,
crate_cfg: cfgs,
crate_check_cfg: options.check_cfgs.clone(),
input,
input: options.input.clone(),
output_file: None,
output_dir: None,
file_loader: None,

View File

@ -730,10 +730,10 @@ fn main_args(
core::new_dcx(options.error_format, None, options.diagnostic_width, &options.unstable_opts);
match (options.should_test, options.markdown_input()) {
(true, true) => return wrap_return(&diag, markdown::test(options)),
(true, false) => return doctest::run(&diag, options),
(false, true) => {
let input = options.input.clone();
(true, Some(_)) => return wrap_return(&diag, markdown::test(options)),
(true, None) => return doctest::run(&diag, options),
(false, Some(input)) => {
let input = input.to_owned();
let edition = options.edition;
let config = core::create_config(options, &render_options, using_internal_features);
@ -747,7 +747,7 @@ fn main_args(
}),
);
}
(false, false) => {}
(false, None) => {}
}
// need to move these items separately because we lose them by the time the closure is called,

View File

@ -144,8 +144,14 @@ pub(crate) fn render<P: AsRef<Path>>(
/// Runs any tests/code examples in the markdown file `input`.
pub(crate) fn test(options: Options) -> Result<(), String> {
let input_str = read_to_string(&options.input)
.map_err(|err| format!("{input}: {err}", input = options.input.display()))?;
use rustc_session::config::Input;
let input_str = match &options.input {
Input::File(path) => {
read_to_string(&path).map_err(|err| format!("{}: {err}", path.display()))?
}
Input::Str { name: _, input } => input.clone(),
};
let mut opts = GlobalTestOptions::default();
opts.no_crate_inject = true;
@ -155,12 +161,12 @@ pub(crate) fn test(options: Options) -> Result<(), String> {
generate_args_file(&file_path, &options)?;
let mut collector = Collector::new(
options.input.display().to_string(),
options.input.filestem().to_string(),
options.clone(),
true,
opts,
None,
Some(options.input),
options.input.opt_path().map(ToOwned::to_owned),
options.enable_per_target_ignores,
file_path,
);

View File

@ -0,0 +1,25 @@
//! This test checks rustdoc `-` (stdin) handling
use run_make_support::{rustdoc, tmp_dir};
static INPUT: &str = r#"
//! ```
//! dbg!(());
//! ```
pub struct F;
"#;
fn main() {
let tmp_dir = tmp_dir();
let out_dir = tmp_dir.join("doc");
// rustdoc -
rustdoc().arg("-").out_dir(&out_dir).stdin(INPUT).run();
assert!(out_dir.join("rust_out/struct.F.html").try_exists().unwrap());
// rustdoc --test -
rustdoc().arg("--test").arg("-").stdin(INPUT).run();
// rustdoc file.rs -
rustdoc().arg("file.rs").arg("-").run_fail();
}