about summary refs log tree commit diff
diff options
context:
space:
mode:
authorQuietMisdreavus <grey@quietmisdreavus.net>2017-09-21 22:37:00 -0500
committerQuietMisdreavus <grey@quietmisdreavus.net>2017-11-21 15:46:49 -0600
commitf9f3611f5c2d8b88361cd67d06528ff1ae7876e9 (patch)
treefc3739e05f0d3669da28533415e134b7a42a5138
parentf28df200260c89b2a0bdf942510e0f888c29a70d (diff)
downloadrust-f9f3611f5c2d8b88361cd67d06528ff1ae7876e9.tar.gz
rust-f9f3611f5c2d8b88361cd67d06528ff1ae7876e9.zip
allow loading external files in documentation
Partial implementation of https://github.com/rust-lang/rfcs/pull/1990
(needs error reporting work)

cc #44732
-rw-r--r--src/doc/unstable-book/src/language-features/external-doc.md40
-rw-r--r--src/librustdoc/clean/mod.rs144
-rw-r--r--src/librustdoc/html/render.rs30
-rw-r--r--src/librustdoc/passes/collapse_docs.rs72
-rw-r--r--src/librustdoc/passes/unindent_comments.rs15
-rw-r--r--src/librustdoc/test.rs10
-rw-r--r--src/libsyntax/ext/base.rs2
-rw-r--r--src/libsyntax/ext/expand.rs88
-rw-r--r--src/libsyntax/feature_gate.rs10
-rw-r--r--src/test/compile-fail/feature-gate-external_doc.rs12
-rw-r--r--src/test/rustdoc/auxiliary/external-cross-doc.md4
-rw-r--r--src/test/rustdoc/auxiliary/external-cross.rs14
-rw-r--r--src/test/rustdoc/auxiliary/external-doc.md3
-rw-r--r--src/test/rustdoc/external-cross.rs20
-rw-r--r--src/test/rustdoc/external-doc.rs18
-rw-r--r--src/test/rustdoc/issue-42760.rs23
16 files changed, 480 insertions, 25 deletions
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 00000000000..effae5d2999
--- /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 35e96745060..42d7a31713c 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<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
+        }
     }
 }
 
diff --git a/src/librustdoc/html/render.rs b/src/librustdoc/html/render.rs
index 8370f805828..077797d1cd0 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<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))?;
         }
     }
 
diff --git a/src/librustdoc/passes/collapse_docs.rs b/src/librustdoc/passes/collapse_docs.rs
index 3c63302127c..a2d651d29de 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<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);
+    }
 }
diff --git a/src/librustdoc/passes/unindent_comments.rs b/src/librustdoc/passes/unindent_comments.rs
index 59fef8d2027..912c7646a06 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<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),
         }
     }
 }
diff --git a/src/librustdoc/test.rs b/src/librustdoc/test.rs
index ea0d32e2a2d..3aa674415f0 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 0e05cce35e2..6c96692f719 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 491dbed01f1..0d1b1c65a29 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<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);
diff --git a/src/libsyntax/feature_gate.rs b/src/libsyntax/feature_gate.rs
index 036c9414990..383fe0092be 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 00000000000..fa0a2a29078
--- /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 <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() {}
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 00000000000..8b4e6edc699
--- /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 00000000000..cb14fec7abe
--- /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 <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;
diff --git a/src/test/rustdoc/auxiliary/external-doc.md b/src/test/rustdoc/auxiliary/external-doc.md
new file mode 100644
index 00000000000..38478c1635a
--- /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 00000000000..f4a59cee32d
--- /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 <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;
diff --git a/src/test/rustdoc/external-doc.rs b/src/test/rustdoc/external-doc.rs
new file mode 100644
index 00000000000..07e784a6ccf
--- /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 <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;
diff --git a/src/test/rustdoc/issue-42760.rs b/src/test/rustdoc/issue-42760.rs
new file mode 100644
index 00000000000..f5f5c4f97fd
--- /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 <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;