mirror of
https://github.com/rust-lang/rust.git
synced 2024-11-02 15:32:06 +00:00
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:
commit
3755fe9555
40
src/doc/unstable-book/src/language-features/external-doc.md
Normal file
40
src/doc/unstable-book/src/language-features/external-doc.md
Normal 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
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
12
src/test/compile-fail/feature-gate-external_doc.rs
Normal file
12
src/test/compile-fail/feature-gate-external_doc.rs
Normal 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() {}
|
4
src/test/rustdoc/auxiliary/external-cross-doc.md
Normal file
4
src/test/rustdoc/auxiliary/external-cross-doc.md
Normal 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.
|
14
src/test/rustdoc/auxiliary/external-cross.rs
Normal file
14
src/test/rustdoc/auxiliary/external-cross.rs
Normal 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;
|
3
src/test/rustdoc/auxiliary/external-doc.md
Normal file
3
src/test/rustdoc/auxiliary/external-doc.md
Normal file
@ -0,0 +1,3 @@
|
||||
# External Docs
|
||||
|
||||
This file is here to test the `#[doc(include="file")]` attribute.
|
20
src/test/rustdoc/external-cross.rs
Normal file
20
src/test/rustdoc/external-cross.rs
Normal 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;
|
18
src/test/rustdoc/external-doc.rs
Normal file
18
src/test/rustdoc/external-doc.rs
Normal 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;
|
23
src/test/rustdoc/issue-42760.rs
Normal file
23
src/test/rustdoc/issue-42760.rs
Normal 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;
|
Loading…
Reference in New Issue
Block a user