Auto merge of #44781 - QuietMisdreavus:doc-include, r=GuillaumeGomez

rustdoc: include external files in documentation (RFC 1990)

Part of https://github.com/rust-lang/rfcs/pull/1990 (needs work on the error reporting, which i'm deferring to after this initial PR)

cc #44732

Also fixes #42760, because the prep work for the error reporting made it easy to fix that at the same time.
This commit is contained in:
bors 2017-11-22 09:58:07 +00:00
commit 3755fe9555
17 changed files with 486 additions and 29 deletions

View File

@ -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

View File

@ -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<String> {
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<I: IntoIterator<Item=ast::NestedMetaItem>> 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<T>(iter: T) -> Self
where
T: IntoIterator<Item = &'a DocFragment>
{
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<String>,
pub doc_strings: Vec<DocFragment>,
pub other_attrs: Vec<ast::Attribute>,
pub cfg: Option<Rc<Cfg>>,
pub span: Option<syntax_pos::Span>,
@ -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<String> = None;
let mut contents: Option<String> = 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<String> {
if !self.doc_strings.is_empty() {
Some(self.doc_strings.iter().collect())
} else {
None
}
}
}

View File

@ -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<Cow<'a, str>> {
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, "<div class='docblock'>{}</div>", prefix)?;
}
@ -3326,8 +3348,8 @@ fn render_impl(w: &mut fmt::Formatter, cx: &Context, i: &Impl, link: AssocItemLi
}
write!(w, "</span>")?;
write!(w, "</h3>\n")?;
if let Some(ref dox) = i.impl_item.doc_value() {
write!(w, "<div class='docblock'>{}</div>", Markdown(dox, cx.render_type))?;
if let Some(ref dox) = cx.shared.maybe_collapsed_doc_value(&i.impl_item) {
write!(w, "<div class='docblock'>{}</div>", Markdown(&*dox, cx.render_type))?;
}
}

View File

@ -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<DocFragment>) {
let mut docs = vec![];
let mut last_frag: Option<DocFragment> = 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);
}
}

View File

@ -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<DocFragment>) {
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),
}
}
}

View File

@ -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));
}
}

View File

@ -371,11 +371,13 @@ impl Attribute {
let meta = mk_name_value_item_str(
Symbol::intern("doc"),
Symbol::intern(&strip_doc_comment_decoration(&comment.as_str())));
if self.style == ast::AttrStyle::Outer {
f(&mk_attr_outer(self.span, self.id, meta))
let mut attr = if self.style == ast::AttrStyle::Outer {
mk_attr_outer(self.span, self.id, meta)
} else {
f(&mk_attr_inner(self.span, self.id, meta))
}
mk_attr_inner(self.span, self.id, meta)
};
attr.is_sugared_doc = true;
f(&attr)
} else {
f(self)
}

View File

@ -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 {

View File

@ -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<ast::Attribute>) -> Option<ast::Attribute> {
@ -1063,6 +1071,84 @@ impl<'a, 'b> Folder for InvocationCollector<'a, 'b> {
}
}
fn fold_attribute(&mut self, at: ast::Attribute) -> Option<ast::Attribute> {
// 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);

View File

@ -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;

View File

@ -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 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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() {}

View File

@ -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.

View File

@ -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 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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;

View File

@ -0,0 +1,3 @@
# External Docs
This file is here to test the `#[doc(include="file")]` attribute.

View File

@ -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 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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;

View File

@ -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 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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;

View File

@ -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 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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;