diff --git a/src/doc/unstable-book/src/language-features/external-doc.md b/src/doc/unstable-book/src/language-features/external-doc.md new file mode 100644 index 0000000000000..effae5d299949 --- /dev/null +++ b/src/doc/unstable-book/src/language-features/external-doc.md @@ -0,0 +1,40 @@ +# `external_doc` + +The tracking issue for this feature is: [#44732] + +The `external_doc` feature allows the use of the `include` parameter to the `#[doc]` attribute, to +include external files in documentation. Use the attribute in place of, or in addition to, regular +doc comments and `#[doc]` attributes, and `rustdoc` will load the given file when it renders +documentation for your crate. + +With the following files in the same directory: + +`external-doc.md`: + +```markdown +# My Awesome Type + +This is the documentation for this spectacular type. +``` + +`lib.rs`: + +```no_run (needs-external-files) +#![feature(external_doc)] + +#[doc(include = "external-doc.md")] +pub struct MyAwesomeType; +``` + +`rustdoc` will load the file `external-doc.md` and use it as the documentation for the `MyAwesomeType` +struct. + +When locating files, `rustdoc` will base paths in the `src/` directory, as if they were alongside the +`lib.rs` for your crate. So if you want a `docs/` folder to live alongside the `src/` directory, +start your paths with `../docs/` for `rustdoc` to properly find the file. + +This feature was proposed in [RFC #1990] and initially implemented in PR [#44781]. + +[#44732]: https://github.com/rust-lang/rust/issues/44732 +[RFC #1990]: https://github.com/rust-lang/rfcs/pull/1990 +[#44781]: https://github.com/rust-lang/rust/pull/44781 diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index 35e9674506014..42d7a31713c90 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -44,6 +44,7 @@ use rustc::hir; use rustc_const_math::ConstInt; use std::{mem, slice, vec}; +use std::iter::FromIterator; use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; @@ -300,6 +301,11 @@ impl Item { pub fn doc_value<'a>(&'a self) -> Option<&'a str> { self.attrs.doc_value() } + /// Finds all `doc` attributes as NameValues and returns their corresponding values, joined + /// with newlines. + pub fn collapsed_doc_value(&self) -> Option { + self.attrs.collapsed_doc_value() + } pub fn is_crate(&self) -> bool { match self.inner { StrippedItem(box ModuleItem(Module { is_crate: true, ..})) | @@ -564,9 +570,69 @@ impl> NestedAttributesExt for I { } } +/// A portion of documentation, extracted from a `#[doc]` attribute. +/// +/// Each variant contains the line number within the complete doc-comment where the fragment +/// starts, as well as the Span where the corresponding doc comment or attribute is located. +/// +/// Included files are kept separate from inline doc comments so that proper line-number +/// information can be given when a doctest fails. Sugared doc comments and "raw" doc comments are +/// kept separate because of issue #42760. +#[derive(Clone, RustcEncodable, RustcDecodable, PartialEq, Debug)] +pub enum DocFragment { + // FIXME #44229 (misdreavus): sugared and raw doc comments can be brought back together once + // hoedown is completely removed from rustdoc. + /// A doc fragment created from a `///` or `//!` doc comment. + SugaredDoc(usize, syntax_pos::Span, String), + /// A doc fragment created from a "raw" `#[doc=""]` attribute. + RawDoc(usize, syntax_pos::Span, String), + /// A doc fragment created from a `#[doc(include="filename")]` attribute. Contains both the + /// given filename and the file contents. + Include(usize, syntax_pos::Span, String, String), +} + +impl DocFragment { + pub fn as_str(&self) -> &str { + match *self { + DocFragment::SugaredDoc(_, _, ref s) => &s[..], + DocFragment::RawDoc(_, _, ref s) => &s[..], + DocFragment::Include(_, _, _, ref s) => &s[..], + } + } + + pub fn span(&self) -> syntax_pos::Span { + match *self { + DocFragment::SugaredDoc(_, span, _) | + DocFragment::RawDoc(_, span, _) | + DocFragment::Include(_, span, _, _) => span, + } + } +} + +impl<'a> FromIterator<&'a DocFragment> for String { + fn from_iter(iter: T) -> Self + where + T: IntoIterator + { + iter.into_iter().fold(String::new(), |mut acc, frag| { + if !acc.is_empty() { + acc.push('\n'); + } + match *frag { + DocFragment::SugaredDoc(_, _, ref docs) + | DocFragment::RawDoc(_, _, ref docs) + | DocFragment::Include(_, _, _, ref docs) => + acc.push_str(docs), + } + + acc + }) + } +} + #[derive(Clone, RustcEncodable, RustcDecodable, PartialEq, Debug, Default)] pub struct Attributes { - pub doc_strings: Vec, + pub doc_strings: Vec, pub other_attrs: Vec, pub cfg: Option>, pub span: Option, @@ -596,6 +662,47 @@ impl Attributes { None } + /// Reads a `MetaItem` from within an attribute, looks for whether it is a + /// `#[doc(include="file")]`, and returns the filename and contents of the file as loaded from + /// its expansion. + fn extract_include(mi: &ast::MetaItem) + -> Option<(String, String)> + { + mi.meta_item_list().and_then(|list| { + for meta in list { + if meta.check_name("include") { + // the actual compiled `#[doc(include="filename")]` gets expanded to + // `#[doc(include(file="filename", contents="file contents")]` so we need to + // look for that instead + return meta.meta_item_list().and_then(|list| { + let mut filename: Option = None; + let mut contents: Option = None; + + for it in list { + if it.check_name("file") { + if let Some(name) = it.value_str() { + filename = Some(name.to_string()); + } + } else if it.check_name("contents") { + if let Some(docs) = it.value_str() { + contents = Some(docs.to_string()); + } + } + } + + if let (Some(filename), Some(contents)) = (filename, contents) { + Some((filename, contents)) + } else { + None + } + }); + } + } + + None + }) + } + pub fn has_doc_flag(&self, flag: &str) -> bool { for attr in &self.other_attrs { if !attr.check_name("doc") { continue; } @@ -610,10 +717,12 @@ impl Attributes { false } - pub fn from_ast(diagnostic: &::errors::Handler, attrs: &[ast::Attribute]) -> Attributes { + pub fn from_ast(diagnostic: &::errors::Handler, + attrs: &[ast::Attribute]) -> Attributes { let mut doc_strings = vec![]; let mut sp = None; let mut cfg = Cfg::True; + let mut doc_line = 0; let other_attrs = attrs.iter().filter_map(|attr| { attr.with_desugared_doc(|attr| { @@ -621,7 +730,16 @@ impl Attributes { if let Some(mi) = attr.meta() { if let Some(value) = mi.value_str() { // Extracted #[doc = "..."] - doc_strings.push(value.to_string()); + let value = value.to_string(); + let line = doc_line; + doc_line += value.lines().count(); + + if attr.is_sugared_doc { + doc_strings.push(DocFragment::SugaredDoc(line, attr.span, value)); + } else { + doc_strings.push(DocFragment::RawDoc(line, attr.span, value)); + } + if sp.is_none() { sp = Some(attr.span); } @@ -633,6 +751,14 @@ impl Attributes { Err(e) => diagnostic.span_err(e.span, e.msg), } return None; + } else if let Some((filename, contents)) = Attributes::extract_include(&mi) + { + let line = doc_line; + doc_line += contents.lines().count(); + doc_strings.push(DocFragment::Include(line, + attr.span, + filename, + contents)); } } } @@ -650,7 +776,17 @@ impl Attributes { /// Finds the `doc` attribute as a NameValue and returns the corresponding /// value found. pub fn doc_value<'a>(&'a self) -> Option<&'a str> { - self.doc_strings.first().map(|s| &s[..]) + self.doc_strings.first().map(|s| s.as_str()) + } + + /// Finds all `doc` attributes as NameValues and returns their corresponding values, joined + /// with newlines. + pub fn collapsed_doc_value(&self) -> Option { + if !self.doc_strings.is_empty() { + Some(self.doc_strings.iter().collect()) + } else { + None + } } } diff --git a/src/librustdoc/html/render.rs b/src/librustdoc/html/render.rs index 8370f80582861..077797d1cd08c 100644 --- a/src/librustdoc/html/render.rs +++ b/src/librustdoc/html/render.rs @@ -36,6 +36,7 @@ pub use self::ExternalLocation::*; #[cfg(stage0)] use std::ascii::AsciiExt; +use std::borrow::Cow; use std::cell::RefCell; use std::cmp::Ordering; use std::collections::{BTreeMap, HashSet}; @@ -143,6 +144,23 @@ impl SharedContext { } } +impl SharedContext { + /// Returns whether the `collapse-docs` pass was run on this crate. + pub fn was_collapsed(&self) -> bool { + self.passes.contains("collapse-docs") + } + + /// Based on whether the `collapse-docs` pass was run, return either the `doc_value` or the + /// `collapsed_doc_value` of the given item. + pub fn maybe_collapsed_doc_value<'a>(&self, item: &'a clean::Item) -> Option> { + if self.was_collapsed() { + item.collapsed_doc_value().map(|s| s.into()) + } else { + item.doc_value().map(|s| s.into()) + } + } +} + /// Indicates where an external crate can be found. pub enum ExternalLocation { /// Remote URL root of the external crate @@ -1817,6 +1835,9 @@ fn plain_summary_line(s: Option<&str>) -> String { } fn document(w: &mut fmt::Formatter, cx: &Context, item: &clean::Item) -> fmt::Result { + if let Some(ref name) = item.name { + info!("Documenting {}", name); + } document_stability(w, cx, item)?; let prefix = render_assoc_const_value(item); document_full(w, item, cx, &prefix)?; @@ -1893,8 +1914,9 @@ fn render_assoc_const_value(item: &clean::Item) -> String { fn document_full(w: &mut fmt::Formatter, item: &clean::Item, cx: &Context, prefix: &str) -> fmt::Result { - if let Some(s) = item.doc_value() { - render_markdown(w, s, item.source.clone(), cx.render_type, prefix, &cx.shared)?; + if let Some(s) = cx.shared.maybe_collapsed_doc_value(item) { + debug!("Doc block: =====\n{}\n=====", s); + render_markdown(w, &*s, item.source.clone(), cx.render_type, prefix, &cx.shared)?; } else if !prefix.is_empty() { write!(w, "
{}
", prefix)?; } @@ -3326,8 +3348,8 @@ fn render_impl(w: &mut fmt::Formatter, cx: &Context, i: &Impl, link: AssocItemLi } write!(w, "")?; write!(w, "\n")?; - if let Some(ref dox) = i.impl_item.doc_value() { - write!(w, "
{}
", Markdown(dox, cx.render_type))?; + if let Some(ref dox) = cx.shared.maybe_collapsed_doc_value(&i.impl_item) { + write!(w, "
{}
", Markdown(&*dox, cx.render_type))?; } } diff --git a/src/librustdoc/passes/collapse_docs.rs b/src/librustdoc/passes/collapse_docs.rs index 3c63302127c5e..a2d651d29de93 100644 --- a/src/librustdoc/passes/collapse_docs.rs +++ b/src/librustdoc/passes/collapse_docs.rs @@ -8,10 +8,28 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use clean::{self, Item}; +use clean::{self, DocFragment, Item}; use plugins; use fold; use fold::DocFolder; +use std::mem::replace; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum DocFragmentKind { + Sugared, + Raw, + Include, +} + +impl DocFragment { + fn kind(&self) -> DocFragmentKind { + match *self { + DocFragment::SugaredDoc(..) => DocFragmentKind::Sugared, + DocFragment::RawDoc(..) => DocFragmentKind::Raw, + DocFragment::Include(..) => DocFragmentKind::Include, + } + } +} pub fn collapse_docs(krate: clean::Crate) -> plugins::PluginResult { Collapser.fold_crate(krate) @@ -26,15 +44,51 @@ impl fold::DocFolder for Collapser { } } -impl clean::Attributes { - pub fn collapse_doc_comments(&mut self) { - let mut doc_string = self.doc_strings.join("\n"); - if doc_string.is_empty() { - self.doc_strings = vec![]; +fn collapse(doc_strings: &mut Vec) { + let mut docs = vec![]; + let mut last_frag: Option = None; + + for frag in replace(doc_strings, vec![]) { + if let Some(mut curr_frag) = last_frag.take() { + let curr_kind = curr_frag.kind(); + let new_kind = frag.kind(); + + if curr_kind == DocFragmentKind::Include || curr_kind != new_kind { + match curr_frag { + DocFragment::SugaredDoc(_, _, ref mut doc_string) + | DocFragment::RawDoc(_, _, ref mut doc_string) => { + // add a newline for extra padding between segments + doc_string.push('\n'); + } + _ => {} + } + docs.push(curr_frag); + last_frag = Some(frag); + } else { + match curr_frag { + DocFragment::SugaredDoc(_, ref mut span, ref mut doc_string) + | DocFragment::RawDoc(_, ref mut span, ref mut doc_string) => { + doc_string.push('\n'); + doc_string.push_str(frag.as_str()); + *span = span.to(frag.span()); + } + _ => unreachable!(), + } + last_frag = Some(curr_frag); + } } else { - // FIXME(eddyb) Is this still needed? - doc_string.push('\n'); - self.doc_strings = vec![doc_string]; + last_frag = Some(frag); } } + + if let Some(frag) = last_frag.take() { + docs.push(frag); + } + *doc_strings = docs; +} + +impl clean::Attributes { + pub fn collapse_doc_comments(&mut self) { + collapse(&mut self.doc_strings); + } } diff --git a/src/librustdoc/passes/unindent_comments.rs b/src/librustdoc/passes/unindent_comments.rs index 59fef8d20271b..912c7646a06e5 100644 --- a/src/librustdoc/passes/unindent_comments.rs +++ b/src/librustdoc/passes/unindent_comments.rs @@ -12,7 +12,7 @@ use std::cmp; use std::string::String; use std::usize; -use clean::{self, Item}; +use clean::{self, DocFragment, Item}; use plugins; use fold::{self, DocFolder}; @@ -31,8 +31,17 @@ impl fold::DocFolder for CommentCleaner { impl clean::Attributes { pub fn unindent_doc_comments(&mut self) { - for doc_string in &mut self.doc_strings { - *doc_string = unindent(doc_string); + unindent_fragments(&mut self.doc_strings); + } +} + +fn unindent_fragments(docs: &mut Vec) { + for fragment in docs { + match *fragment { + DocFragment::SugaredDoc(_, _, ref mut doc_string) | + DocFragment::RawDoc(_, _, ref mut doc_string) | + DocFragment::Include(_, _, _, ref mut doc_string) => + *doc_string = unindent(doc_string), } } } diff --git a/src/librustdoc/test.rs b/src/librustdoc/test.rs index ea0d32e2a2d56..3aa674415f03b 100644 --- a/src/librustdoc/test.rs +++ b/src/librustdoc/test.rs @@ -663,14 +663,16 @@ impl<'a, 'hir> HirCollector<'a, 'hir> { attrs.collapse_doc_comments(); attrs.unindent_doc_comments(); - if let Some(doc) = attrs.doc_value() { + // the collapse-docs pass won't combine sugared/raw doc attributes, or included files with + // anything else, this will combine them for us + if let Some(doc) = attrs.collapsed_doc_value() { if self.collector.render_type == RenderType::Pulldown { - markdown::old_find_testable_code(doc, self.collector, + markdown::old_find_testable_code(&doc, self.collector, attrs.span.unwrap_or(DUMMY_SP)); - markdown::find_testable_code(doc, self.collector, + markdown::find_testable_code(&doc, self.collector, attrs.span.unwrap_or(DUMMY_SP)); } else { - markdown::old_find_testable_code(doc, self.collector, + markdown::old_find_testable_code(&doc, self.collector, attrs.span.unwrap_or(DUMMY_SP)); } } diff --git a/src/libsyntax/ext/base.rs b/src/libsyntax/ext/base.rs index 0e05cce35e2df..6c96692f719ff 100644 --- a/src/libsyntax/ext/base.rs +++ b/src/libsyntax/ext/base.rs @@ -665,6 +665,7 @@ pub struct ExtCtxt<'a> { pub parse_sess: &'a parse::ParseSess, pub ecfg: expand::ExpansionConfig<'a>, pub crate_root: Option<&'static str>, + pub root_path: PathBuf, pub resolver: &'a mut Resolver, pub resolve_err_count: usize, pub current_expansion: ExpansionData, @@ -680,6 +681,7 @@ impl<'a> ExtCtxt<'a> { parse_sess, ecfg, crate_root: None, + root_path: PathBuf::new(), resolver, resolve_err_count: 0, current_expansion: ExpansionData { diff --git a/src/libsyntax/ext/expand.rs b/src/libsyntax/ext/expand.rs index 491dbed01f127..0d1b1c65a2934 100644 --- a/src/libsyntax/ext/expand.rs +++ b/src/libsyntax/ext/expand.rs @@ -11,7 +11,7 @@ use ast::{self, Block, Ident, NodeId, PatKind, Path}; use ast::{MacStmtStyle, StmtKind, ItemKind}; use attr::{self, HasAttrs}; -use codemap::{ExpnInfo, NameAndSpan, MacroBang, MacroAttribute}; +use codemap::{ExpnInfo, NameAndSpan, MacroBang, MacroAttribute, dummy_spanned}; use config::{is_test_or_bench, StripUnconfigured}; use errors::FatalError; use ext::base::*; @@ -35,6 +35,8 @@ use util::small_vector::SmallVector; use visit::Visitor; use std::collections::HashMap; +use std::fs::File; +use std::io::Read; use std::mem; use std::rc::Rc; @@ -223,6 +225,7 @@ impl<'a, 'b> MacroExpander<'a, 'b> { directory: self.cx.codemap().span_to_unmapped_path(krate.span), }; module.directory.pop(); + self.cx.root_path = module.directory.clone(); self.cx.current_expansion.module = Rc::new(module); let orig_mod_span = krate.module.inner; @@ -843,6 +846,11 @@ impl<'a, 'b> InvocationCollector<'a, 'b> { feature_gate::check_attribute(attr, self.cx.parse_sess, features); } } + + fn check_attribute(&mut self, at: &ast::Attribute) { + let features = self.cx.ecfg.features.unwrap(); + feature_gate::check_attribute(at, self.cx.parse_sess, features); + } } pub fn find_attr_invoc(attrs: &mut Vec) -> Option { @@ -1063,6 +1071,84 @@ impl<'a, 'b> Folder for InvocationCollector<'a, 'b> { } } + fn fold_attribute(&mut self, at: ast::Attribute) -> Option { + // turn `#[doc(include="filename")]` attributes into `#[doc(include(file="filename", + // contents="file contents")]` attributes + if !at.check_name("doc") { + return noop_fold_attribute(at, self); + } + + if let Some(list) = at.meta_item_list() { + if !list.iter().any(|it| it.check_name("include")) { + return noop_fold_attribute(at, self); + } + + let mut items = vec![]; + + for it in list { + if !it.check_name("include") { + items.push(noop_fold_meta_list_item(it, self)); + continue; + } + + if let Some(file) = it.value_str() { + let err_count = self.cx.parse_sess.span_diagnostic.err_count(); + self.check_attribute(&at); + if self.cx.parse_sess.span_diagnostic.err_count() > err_count { + // avoid loading the file if they haven't enabled the feature + return noop_fold_attribute(at, self); + } + + let mut buf = vec![]; + let filename = self.cx.root_path.join(file.to_string()); + + match File::open(&filename).and_then(|mut f| f.read_to_end(&mut buf)) { + Ok(..) => {} + Err(e) => { + self.cx.span_warn(at.span, + &format!("couldn't read {}: {}", + filename.display(), + e)); + } + } + + match String::from_utf8(buf) { + Ok(src) => { + let include_info = vec![ + dummy_spanned(ast::NestedMetaItemKind::MetaItem( + attr::mk_name_value_item_str("file".into(), + file))), + dummy_spanned(ast::NestedMetaItemKind::MetaItem( + attr::mk_name_value_item_str("contents".into(), + (&*src).into()))), + ]; + + items.push(dummy_spanned(ast::NestedMetaItemKind::MetaItem( + attr::mk_list_item("include".into(), include_info)))); + } + Err(_) => { + self.cx.span_warn(at.span, + &format!("{} wasn't a utf-8 file", + filename.display())); + } + } + } else { + items.push(noop_fold_meta_list_item(it, self)); + } + } + + let meta = attr::mk_list_item("doc".into(), items); + match at.style { + ast::AttrStyle::Inner => + Some(attr::mk_spanned_attr_inner(at.span, at.id, meta)), + ast::AttrStyle::Outer => + Some(attr::mk_spanned_attr_outer(at.span, at.id, meta)), + } + } else { + noop_fold_attribute(at, self) + } + } + fn new_id(&mut self, id: ast::NodeId) -> ast::NodeId { if self.monotonic { assert_eq!(id, ast::DUMMY_NODE_ID); diff --git a/src/libsyntax/feature_gate.rs b/src/libsyntax/feature_gate.rs index 036c941499048..383fe0092be57 100644 --- a/src/libsyntax/feature_gate.rs +++ b/src/libsyntax/feature_gate.rs @@ -383,6 +383,8 @@ declare_features! ( (active, doc_masked, "1.21.0", Some(44027)), // #[doc(spotlight)] (active, doc_spotlight, "1.22.0", Some(45040)), + // #[doc(include="some-file")] + (active, external_doc, "1.22.0", Some(44732)), // allow `#[must_use]` on functions and comparison operators (RFC 1940) (active, fn_must_use, "1.21.0", Some(43302)), @@ -1028,6 +1030,14 @@ impl<'a> Context<'a> { if name == n { if let Gated(_, name, desc, ref has_feature) = *gateage { gate_feature_fn!(self, has_feature, attr.span, name, desc, GateStrength::Hard); + } else if name == "doc" { + if let Some(content) = attr.meta_item_list() { + if content.iter().any(|c| c.check_name("include")) { + gate_feature!(self, external_doc, attr.span, + "#[doc(include = \"...\")] is experimental" + ); + } + } } debug!("check_attribute: {:?} is builtin, {:?}, {:?}", attr.path, ty, gateage); return; diff --git a/src/test/compile-fail/feature-gate-external_doc.rs b/src/test/compile-fail/feature-gate-external_doc.rs new file mode 100644 index 0000000000000..fa0a2a29078c5 --- /dev/null +++ b/src/test/compile-fail/feature-gate-external_doc.rs @@ -0,0 +1,12 @@ +// Copyright 2017 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[doc(include="asdf.md")] //~ ERROR: #[doc(include = "...")] is experimental +fn main() {} diff --git a/src/test/rustdoc/auxiliary/external-cross-doc.md b/src/test/rustdoc/auxiliary/external-cross-doc.md new file mode 100644 index 0000000000000..8b4e6edc69997 --- /dev/null +++ b/src/test/rustdoc/auxiliary/external-cross-doc.md @@ -0,0 +1,4 @@ +# Cross-crate imported docs + +This file is to make sure `#[doc(include="file.md")]` works when you re-export an item with included +docs. diff --git a/src/test/rustdoc/auxiliary/external-cross.rs b/src/test/rustdoc/auxiliary/external-cross.rs new file mode 100644 index 0000000000000..cb14fec7abe7a --- /dev/null +++ b/src/test/rustdoc/auxiliary/external-cross.rs @@ -0,0 +1,14 @@ +// Copyright 2017 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![feature(external_doc)] + +#[doc(include="external-cross-doc.md")] +pub struct NeedMoreDocs; diff --git a/src/test/rustdoc/auxiliary/external-doc.md b/src/test/rustdoc/auxiliary/external-doc.md new file mode 100644 index 0000000000000..38478c1635a17 --- /dev/null +++ b/src/test/rustdoc/auxiliary/external-doc.md @@ -0,0 +1,3 @@ +# External Docs + +This file is here to test the `#[doc(include="file")]` attribute. diff --git a/src/test/rustdoc/external-cross.rs b/src/test/rustdoc/external-cross.rs new file mode 100644 index 0000000000000..f4a59cee32dd9 --- /dev/null +++ b/src/test/rustdoc/external-cross.rs @@ -0,0 +1,20 @@ +// Copyright 2017 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +// aux-build:external-cross.rs +// ignore-cross-compile + +#![crate_name="host"] + +extern crate external_cross; + +// @has host/struct.NeedMoreDocs.html +// @has - '//h1' 'Cross-crate imported docs' +pub use external_cross::NeedMoreDocs; diff --git a/src/test/rustdoc/external-doc.rs b/src/test/rustdoc/external-doc.rs new file mode 100644 index 0000000000000..07e784a6ccfaf --- /dev/null +++ b/src/test/rustdoc/external-doc.rs @@ -0,0 +1,18 @@ +// Copyright 2017 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![feature(external_doc)] + +// @has external_doc/struct.CanHasDocs.html +// @has - '//h1' 'External Docs' +// @has - '//h2' 'Inline Docs' +#[doc(include = "auxiliary/external-doc.md")] +/// ## Inline Docs +pub struct CanHasDocs; diff --git a/src/test/rustdoc/issue-42760.rs b/src/test/rustdoc/issue-42760.rs new file mode 100644 index 0000000000000..f5f5c4f97fd02 --- /dev/null +++ b/src/test/rustdoc/issue-42760.rs @@ -0,0 +1,23 @@ +// Copyright 2017 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +// @has issue_42760/struct.NonGen.html +// @has - '//h1' 'Example' + +/// Item docs. +/// +#[doc="Hello there!"] +/// +/// # Example +/// +/// ```rust +/// // some code here +/// ``` +pub struct NonGen;