diff options
Diffstat (limited to '')
-rw-r--r-- | src/librustdoc/passes/bare_urls.rs | 6 | ||||
-rw-r--r-- | src/librustdoc/passes/calculate_doc_coverage.rs | 18 | ||||
-rw-r--r-- | src/librustdoc/passes/check_code_block_syntax.rs | 112 | ||||
-rw-r--r-- | src/librustdoc/passes/check_doc_test_visibility.rs | 16 | ||||
-rw-r--r-- | src/librustdoc/passes/collect_intra_doc_links.rs | 48 | ||||
-rw-r--r-- | src/librustdoc/passes/collect_intra_doc_links/early.rs | 44 | ||||
-rw-r--r-- | src/librustdoc/passes/html_tags.rs | 153 | ||||
-rw-r--r-- | src/librustdoc/passes/propagate_doc_cfg.rs | 62 | ||||
-rw-r--r-- | src/librustdoc/passes/strip_hidden.rs | 8 | ||||
-rw-r--r-- | src/librustdoc/passes/strip_private.rs | 12 | ||||
-rw-r--r-- | src/librustdoc/passes/stripper.rs | 99 |
11 files changed, 414 insertions, 164 deletions
diff --git a/src/librustdoc/passes/bare_urls.rs b/src/librustdoc/passes/bare_urls.rs index 392e26ea6..7ff3ccef9 100644 --- a/src/librustdoc/passes/bare_urls.rs +++ b/src/librustdoc/passes/bare_urls.rs @@ -71,16 +71,14 @@ impl<'a, 'tcx> DocVisitor for BareUrlsLinter<'a, 'tcx> { let report_diag = |cx: &DocContext<'_>, msg: &str, url: &str, range: Range<usize>| { let sp = super::source_span_for_markdown_range(cx.tcx, &dox, &range, &item.attrs) .unwrap_or_else(|| item.attr_span(cx.tcx)); - cx.tcx.struct_span_lint_hir(crate::lint::BARE_URLS, hir_id, sp, |lint| { - lint.build(msg) - .note("bare URLs are not automatically turned into clickable links") + cx.tcx.struct_span_lint_hir(crate::lint::BARE_URLS, hir_id, sp, msg, |lint| { + lint.note("bare URLs are not automatically turned into clickable links") .span_suggestion( sp, "use an automatic link instead", format!("<{}>", url), Applicability::MachineApplicable, ) - .emit(); }); }; diff --git a/src/librustdoc/passes/calculate_doc_coverage.rs b/src/librustdoc/passes/calculate_doc_coverage.rs index 4c6e3eb04..48835abf9 100644 --- a/src/librustdoc/passes/calculate_doc_coverage.rs +++ b/src/librustdoc/passes/calculate_doc_coverage.rs @@ -215,7 +215,6 @@ impl<'a, 'b> DocVisitor for CoverageCalculator<'a, 'b> { None, ); - let filename = i.span(self.ctx.tcx).filename(self.ctx.sess()); let has_doc_example = tests.found_tests != 0; // The `expect_def_id()` should be okay because `local_def_id_to_hir_id` // would presumably panic if a fake `DefIndex` were passed. @@ -261,13 +260,16 @@ impl<'a, 'b> DocVisitor for CoverageCalculator<'a, 'b> { let should_have_docs = !should_be_ignored && (level != lint::Level::Allow || matches!(source, LintLevelSource::Default)); - debug!("counting {:?} {:?} in {:?}", i.type_(), i.name, filename); - self.items.entry(filename).or_default().count_item( - has_docs, - has_doc_example, - should_have_doc_example(self.ctx, i), - should_have_docs, - ); + if let Some(span) = i.span(self.ctx.tcx) { + let filename = span.filename(self.ctx.sess()); + debug!("counting {:?} {:?} in {:?}", i.type_(), i.name, filename); + self.items.entry(filename).or_default().count_item( + has_docs, + has_doc_example, + should_have_doc_example(self.ctx, i), + should_have_docs, + ); + } } } diff --git a/src/librustdoc/passes/check_code_block_syntax.rs b/src/librustdoc/passes/check_code_block_syntax.rs index 0172ef570..2e651b538 100644 --- a/src/librustdoc/passes/check_code_block_syntax.rs +++ b/src/librustdoc/passes/check_code_block_syntax.rs @@ -1,7 +1,9 @@ //! Validates syntax inside Rust code blocks (\`\`\`rust). use rustc_data_structures::sync::{Lock, Lrc}; use rustc_errors::{ - emitter::Emitter, Applicability, Diagnostic, Handler, LazyFallbackBundle, LintDiagnosticBuilder, + emitter::Emitter, + translation::{to_fluent_args, Translate}, + Applicability, Diagnostic, Handler, LazyFallbackBundle, }; use rustc_parse::parse_stream_from_source_str; use rustc_session::parse::ParseSess; @@ -96,48 +98,10 @@ impl<'a, 'tcx> SyntaxChecker<'a, 'tcx> { None => (item.attr_span(self.cx.tcx), false), }; - // lambda that will use the lint to start a new diagnostic and add - // a suggestion to it when needed. - let diag_builder = |lint: LintDiagnosticBuilder<'_, ()>| { - let explanation = if is_ignore { - "`ignore` code blocks require valid Rust code for syntax highlighting; \ - mark blocks that do not contain Rust code as text" - } else { - "mark blocks that do not contain Rust code as text" - }; - let msg = if buffer.has_errors { - "could not parse code block as Rust code" - } else { - "Rust code block is empty" - }; - let mut diag = lint.build(msg); - - if precise_span { - if is_ignore { - // giving an accurate suggestion is hard because `ignore` might not have come first in the list. - // just give a `help` instead. - diag.span_help( - sp.from_inner(InnerSpan::new(0, 3)), - &format!("{}: ```text", explanation), - ); - } else if empty_block { - diag.span_suggestion( - sp.from_inner(InnerSpan::new(0, 3)).shrink_to_hi(), - explanation, - "text", - Applicability::MachineApplicable, - ); - } - } else if empty_block || is_ignore { - diag.help(&format!("{}: ```text", explanation)); - } - - // FIXME(#67563): Provide more context for these errors by displaying the spans inline. - for message in buffer.messages.iter() { - diag.note(message); - } - - diag.emit(); + let msg = if buffer.has_errors { + "could not parse code block as Rust code" + } else { + "Rust code block is empty" }; // Finally build and emit the completed diagnostic. @@ -147,7 +111,42 @@ impl<'a, 'tcx> SyntaxChecker<'a, 'tcx> { crate::lint::INVALID_RUST_CODEBLOCKS, hir_id, sp, - diag_builder, + msg, + |lint| { + let explanation = if is_ignore { + "`ignore` code blocks require valid Rust code for syntax highlighting; \ + mark blocks that do not contain Rust code as text" + } else { + "mark blocks that do not contain Rust code as text" + }; + + if precise_span { + if is_ignore { + // giving an accurate suggestion is hard because `ignore` might not have come first in the list. + // just give a `help` instead. + lint.span_help( + sp.from_inner(InnerSpan::new(0, 3)), + &format!("{}: ```text", explanation), + ); + } else if empty_block { + lint.span_suggestion( + sp.from_inner(InnerSpan::new(0, 3)).shrink_to_hi(), + explanation, + "text", + Applicability::MachineApplicable, + ); + } + } else if empty_block || is_ignore { + lint.help(&format!("{}: ```text", explanation)); + } + + // FIXME(#67563): Provide more context for these errors by displaying the spans inline. + for message in buffer.messages.iter() { + lint.note(message); + } + + lint + }, ); } } @@ -181,11 +180,24 @@ struct BufferEmitter { fallback_bundle: LazyFallbackBundle, } +impl Translate for BufferEmitter { + fn fluent_bundle(&self) -> Option<&Lrc<rustc_errors::FluentBundle>> { + None + } + + fn fallback_fluent_bundle(&self) -> &rustc_errors::FluentBundle { + &**self.fallback_bundle + } +} + impl Emitter for BufferEmitter { fn emit_diagnostic(&mut self, diag: &Diagnostic) { let mut buffer = self.buffer.borrow_mut(); - // FIXME(davidtwco): need to support translation here eventually - buffer.messages.push(format!("error from rustc: {}", diag.message[0].0.expect_str())); + + let fluent_args = to_fluent_args(diag.args()); + let translated_main_message = self.translate_message(&diag.message[0].0, &fluent_args); + + buffer.messages.push(format!("error from rustc: {}", translated_main_message)); if diag.is_error() { buffer.has_errors = true; } @@ -194,12 +206,4 @@ impl Emitter for BufferEmitter { fn source_map(&self) -> Option<&Lrc<SourceMap>> { None } - - fn fluent_bundle(&self) -> Option<&Lrc<rustc_errors::FluentBundle>> { - None - } - - fn fallback_fluent_bundle(&self) -> &rustc_errors::FluentBundle { - &**self.fallback_bundle - } } diff --git a/src/librustdoc/passes/check_doc_test_visibility.rs b/src/librustdoc/passes/check_doc_test_visibility.rs index e86f90833..7740c6d5b 100644 --- a/src/librustdoc/passes/check_doc_test_visibility.rs +++ b/src/librustdoc/passes/check_doc_test_visibility.rs @@ -56,7 +56,7 @@ impl crate::doctest::Tester for Tests { } pub(crate) fn should_have_doc_example(cx: &DocContext<'_>, item: &clean::Item) -> bool { - if !cx.cache.access_levels.is_public(item.item_id.expect_def_id()) + if !cx.cache.effective_visibilities.is_directly_public(item.item_id.expect_def_id()) || matches!( *item.kind, clean::StructFieldItem(_) @@ -117,7 +117,7 @@ pub(crate) fn look_for_tests<'tcx>(cx: &DocContext<'tcx>, dox: &str, item: &Item find_testable_code(dox, &mut tests, ErrorCodes::No, false, None); - if tests.found_tests == 0 && cx.tcx.sess.is_nightly_build() { + if tests.found_tests == 0 && cx.tcx.features().rustdoc_missing_doc_code_examples { if should_have_doc_example(cx, item) { debug!("reporting error for {:?} (hir_id={:?})", item, hir_id); let sp = item.attr_span(cx.tcx); @@ -125,21 +125,19 @@ pub(crate) fn look_for_tests<'tcx>(cx: &DocContext<'tcx>, dox: &str, item: &Item crate::lint::MISSING_DOC_CODE_EXAMPLES, hir_id, sp, - |lint| { - lint.build("missing code example in this documentation").emit(); - }, + "missing code example in this documentation", + |lint| lint, ); } } else if tests.found_tests > 0 - && !cx.cache.access_levels.is_exported(item.item_id.expect_def_id()) + && !cx.cache.effective_visibilities.is_exported(item.item_id.expect_def_id()) { cx.tcx.struct_span_lint_hir( crate::lint::PRIVATE_DOC_TESTS, hir_id, item.attr_span(cx.tcx), - |lint| { - lint.build("documentation test in private item").emit(); - }, + "documentation test in private item", + |lint| lint, ); } } diff --git a/src/librustdoc/passes/collect_intra_doc_links.rs b/src/librustdoc/passes/collect_intra_doc_links.rs index 7d7a63c53..8aa0abd36 100644 --- a/src/librustdoc/passes/collect_intra_doc_links.rs +++ b/src/librustdoc/passes/collect_intra_doc_links.rs @@ -80,10 +80,10 @@ impl Res { } } - fn def_id(self, tcx: TyCtxt<'_>) -> DefId { + fn def_id(self, tcx: TyCtxt<'_>) -> Option<DefId> { match self { - Res::Def(_, id) => id, - Res::Primitive(prim) => *PrimitiveType::primitive_locations(tcx).get(&prim).unwrap(), + Res::Def(_, id) => Some(id), + Res::Primitive(prim) => PrimitiveType::primitive_locations(tcx).get(&prim).copied(), } } @@ -223,6 +223,9 @@ enum MalformedGenerics { #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub(crate) enum UrlFragment { Item(DefId), + /// A part of a page that isn't a rust item. + /// + /// Eg: `[Vector Examples](std::vec::Vec#examples)` UserWritten(String), } @@ -477,7 +480,7 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> { // If there's no `::`, it's not an associated item. // So we can be sure that `rustc_resolve` was accurate when it said it wasn't resolved. .ok_or_else(|| { - debug!("found no `::`, assumming {} was correctly not in scope", item_name); + debug!("found no `::`, assuming {} was correctly not in scope", item_name); UnresolvedPath { item_id, module_id, @@ -750,7 +753,7 @@ fn resolve_associated_trait_item<'a>( /// /// This is just a wrapper around [`TyCtxt::impl_item_implementor_ids()`] and /// [`TyCtxt::associated_item()`] (with some helpful logging added). -#[instrument(level = "debug", skip(tcx))] +#[instrument(level = "debug", skip(tcx), ret)] fn trait_assoc_to_impl_assoc_item<'tcx>( tcx: TyCtxt<'tcx>, impl_id: DefId, @@ -760,9 +763,7 @@ fn trait_assoc_to_impl_assoc_item<'tcx>( debug!(?trait_to_impl_assoc_map); let impl_assoc_id = *trait_to_impl_assoc_map.get(&trait_assoc_id)?; debug!(?impl_assoc_id); - let impl_assoc = tcx.associated_item(impl_assoc_id); - debug!(?impl_assoc); - Some(impl_assoc) + Some(tcx.associated_item(impl_assoc_id)) } /// Given a type, return all trait impls in scope in `module` for that type. @@ -1126,10 +1127,10 @@ impl LinkCollector<'_, '_> { } } - Some(ItemLink { + res.def_id(self.cx.tcx).map(|page_id| ItemLink { link: ori_link.link.clone(), link_text: link_text.clone(), - did: res.def_id(self.cx.tcx), + page_id, fragment, }) } @@ -1148,11 +1149,12 @@ impl LinkCollector<'_, '_> { item, &diag_info, )?; - let id = clean::register_res(self.cx, rustc_hir::def::Res::Def(kind, id)); + + let page_id = clean::register_res(self.cx, rustc_hir::def::Res::Def(kind, id)); Some(ItemLink { link: ori_link.link.clone(), link_text: link_text.clone(), - did: id, + page_id, fragment, }) } @@ -1200,8 +1202,8 @@ impl LinkCollector<'_, '_> { item.item_id.expect_def_id().as_local().map(|src_id| (src_id, dst_id)) }) { - if self.cx.tcx.privacy_access_levels(()).is_exported(src_id) - && !self.cx.tcx.privacy_access_levels(()).is_exported(dst_id) + if self.cx.tcx.effective_visibilities(()).is_exported(src_id) + && !self.cx.tcx.effective_visibilities(()).is_exported(dst_id) { privacy_error(self.cx, diag_info, path_str); } @@ -1256,7 +1258,7 @@ impl LinkCollector<'_, '_> { &mut self, key: ResolutionInfo, diag: DiagnosticInfo<'_>, - // If errors are cached then they are only reported on first ocurrence + // If errors are cached then they are only reported on first occurrence // which we want in some cases but not in others. cache_errors: bool, ) -> Option<(Res, Option<UrlFragment>)> { @@ -1607,9 +1609,7 @@ fn report_diagnostic( let sp = item.attr_span(tcx); - tcx.struct_span_lint_hir(lint, hir_id, sp, |lint| { - let mut diag = lint.build(msg); - + tcx.struct_span_lint_hir(lint, hir_id, sp, msg, |lint| { let span = super::source_span_for_markdown_range(tcx, dox, link_range, &item.attrs).map(|sp| { if dox.as_bytes().get(link_range.start) == Some(&b'`') @@ -1622,7 +1622,7 @@ fn report_diagnostic( }); if let Some(sp) = span { - diag.set_span(sp); + lint.set_span(sp); } else { // blah blah blah\nblah\nblah [blah] blah blah\nblah blah // ^ ~~~~ @@ -1632,7 +1632,7 @@ fn report_diagnostic( let line = dox[last_new_line_offset..].lines().next().unwrap_or(""); // Print the line containing the `link_range` and manually mark it with '^'s. - diag.note(&format!( + lint.note(&format!( "the link appears in this line:\n\n{line}\n\ {indicator: <before$}{indicator:^<found$}", line = line, @@ -1642,9 +1642,9 @@ fn report_diagnostic( )); } - decorate(&mut diag, span); + decorate(lint, span); - diag.emit(); + lint }); } @@ -1807,8 +1807,8 @@ fn resolution_failure( } return; } - Trait | TyAlias | ForeignTy | OpaqueTy | TraitAlias | TyParam - | Static(_) => "associated item", + Trait | TyAlias | ForeignTy | OpaqueTy | ImplTraitPlaceholder + | TraitAlias | TyParam | Static(_) => "associated item", Impl | GlobalAsm => unreachable!("not a path"), } } else { diff --git a/src/librustdoc/passes/collect_intra_doc_links/early.rs b/src/librustdoc/passes/collect_intra_doc_links/early.rs index 38cfd7a27..1b373cfe5 100644 --- a/src/librustdoc/passes/collect_intra_doc_links/early.rs +++ b/src/librustdoc/passes/collect_intra_doc_links/early.rs @@ -37,7 +37,6 @@ pub(crate) fn early_resolve_intra_doc_links( markdown_links: Default::default(), doc_link_resolutions: Default::default(), traits_in_scope: Default::default(), - all_traits: Default::default(), all_trait_impls: Default::default(), all_macro_rules: Default::default(), document_private_items, @@ -48,7 +47,6 @@ pub(crate) fn early_resolve_intra_doc_links( link_resolver.resolve_doc_links_local(&krate.attrs); link_resolver.process_module_children_or_reexports(CRATE_DEF_ID.to_def_id()); visit::walk_crate(&mut link_resolver, krate); - link_resolver.process_extern_impls(); // FIXME: somehow rustdoc is still missing crates even though we loaded all // the known necessary crates. Load them all unconditionally until we find a way to fix this. @@ -58,11 +56,12 @@ pub(crate) fn early_resolve_intra_doc_links( link_resolver.resolver.resolve_rustdoc_path(extern_name, TypeNS, parent_scope); } + link_resolver.process_extern_impls(); + ResolverCaches { markdown_links: Some(link_resolver.markdown_links), doc_link_resolutions: link_resolver.doc_link_resolutions, traits_in_scope: link_resolver.traits_in_scope, - all_traits: Some(link_resolver.all_traits), all_trait_impls: Some(link_resolver.all_trait_impls), all_macro_rules: link_resolver.all_macro_rules, } @@ -80,7 +79,6 @@ struct EarlyDocLinkResolver<'r, 'ra> { markdown_links: FxHashMap<String, Vec<PreprocessedMarkdownLink>>, doc_link_resolutions: FxHashMap<(Symbol, Namespace, DefId), Option<Res<ast::NodeId>>>, traits_in_scope: DefIdMap<Vec<TraitCandidate>>, - all_traits: Vec<DefId>, all_trait_impls: Vec<DefId>, all_macro_rules: FxHashMap<Symbol, Res<ast::NodeId>>, document_private_items: bool, @@ -121,8 +119,6 @@ impl<'ra> EarlyDocLinkResolver<'_, 'ra> { loop { let crates = Vec::from_iter(self.resolver.cstore().crates_untracked()); for &cnum in &crates[start_cnum..] { - let all_traits = - Vec::from_iter(self.resolver.cstore().traits_in_crate_untracked(cnum)); let all_trait_impls = Vec::from_iter(self.resolver.cstore().trait_impls_in_crate_untracked(cnum)); let all_inherent_impls = @@ -131,20 +127,18 @@ impl<'ra> EarlyDocLinkResolver<'_, 'ra> { self.resolver.cstore().incoherent_impls_in_crate_untracked(cnum), ); - // Querying traits in scope is expensive so we try to prune the impl and traits lists - // using privacy, private traits and impls from other crates are never documented in + // Querying traits in scope is expensive so we try to prune the impl lists using + // privacy, private traits and impls from other crates are never documented in // the current crate, and links in their doc comments are not resolved. - for &def_id in &all_traits { - if self.resolver.cstore().visibility_untracked(def_id).is_public() { - self.resolve_doc_links_extern_impl(def_id, false); - } - } for &(trait_def_id, impl_def_id, simplified_self_ty) in &all_trait_impls { if self.resolver.cstore().visibility_untracked(trait_def_id).is_public() && simplified_self_ty.and_then(|ty| ty.def()).map_or(true, |ty_def_id| { self.resolver.cstore().visibility_untracked(ty_def_id).is_public() }) { + if self.visited_mods.insert(trait_def_id) { + self.resolve_doc_links_extern_impl(trait_def_id, false); + } self.resolve_doc_links_extern_impl(impl_def_id, false); } } @@ -157,7 +151,6 @@ impl<'ra> EarlyDocLinkResolver<'_, 'ra> { self.resolve_doc_links_extern_impl(impl_def_id, true); } - self.all_traits.extend(all_traits); self.all_trait_impls .extend(all_trait_impls.into_iter().map(|(_, def_id, _)| def_id)); } @@ -306,15 +299,20 @@ impl<'ra> EarlyDocLinkResolver<'_, 'ra> { { if let Some(def_id) = child.res.opt_def_id() && !def_id.is_local() { let scope_id = match child.res { - Res::Def(DefKind::Variant, ..) => self.resolver.parent(def_id), + Res::Def( + DefKind::Variant + | DefKind::AssocTy + | DefKind::AssocFn + | DefKind::AssocConst, + .., + ) => self.resolver.parent(def_id), _ => def_id, }; self.resolve_doc_links_extern_outer(def_id, scope_id); // Outer attribute scope if let Res::Def(DefKind::Mod, ..) = child.res { self.resolve_doc_links_extern_inner(def_id); // Inner attribute scope } - // `DefKind::Trait`s are processed in `process_extern_impls`. - if let Res::Def(DefKind::Mod | DefKind::Enum, ..) = child.res { + if let Res::Def(DefKind::Mod | DefKind::Enum | DefKind::Trait, ..) = child.res { self.process_module_children_or_reexports(def_id); } if let Res::Def(DefKind::Struct | DefKind::Union | DefKind::Variant, _) = @@ -356,10 +354,14 @@ impl Visitor<'_> for EarlyDocLinkResolver<'_, '_> { self.parent_scope.module = old_module; } else { match &item.kind { - ItemKind::Trait(..) => { - self.all_traits.push(self.resolver.local_def_id(item.id).to_def_id()); - } - ItemKind::Impl(box ast::Impl { of_trait: Some(..), .. }) => { + ItemKind::Impl(box ast::Impl { of_trait: Some(trait_ref), .. }) => { + if let Some(partial_res) = self.resolver.get_partial_res(trait_ref.ref_id) + && let Some(res) = partial_res.full_res() + && let Some(trait_def_id) = res.opt_def_id() + && !trait_def_id.is_local() + && self.visited_mods.insert(trait_def_id) { + self.resolve_doc_links_extern_impl(trait_def_id, false); + } self.all_trait_impls.push(self.resolver.local_def_id(item.id).to_def_id()); } ItemKind::MacroDef(macro_def) if macro_def.macro_rules => { diff --git a/src/librustdoc/passes/html_tags.rs b/src/librustdoc/passes/html_tags.rs index f3a3c853c..a89ed7c7e 100644 --- a/src/librustdoc/passes/html_tags.rs +++ b/src/librustdoc/passes/html_tags.rs @@ -22,10 +22,8 @@ struct InvalidHtmlTagsLinter<'a, 'tcx> { } pub(crate) fn check_invalid_html_tags(krate: Crate, cx: &mut DocContext<'_>) -> Crate { - if cx.tcx.sess.is_nightly_build() { - let mut coll = InvalidHtmlTagsLinter { cx }; - coll.visit_crate(&krate); - } + let mut coll = InvalidHtmlTagsLinter { cx }; + coll.visit_crate(&krate); krate } @@ -94,6 +92,34 @@ fn extract_path_backwards(text: &str, end_pos: usize) -> Option<usize> { if current_pos == end_pos { None } else { Some(current_pos) } } +fn extract_path_forward(text: &str, start_pos: usize) -> Option<usize> { + use rustc_lexer::{is_id_continue, is_id_start}; + let mut current_pos = start_pos; + loop { + if current_pos < text.len() && text[current_pos..].starts_with("::") { + current_pos += 2; + } else { + break; + } + let mut chars = text[current_pos..].chars(); + if let Some(c) = chars.next() { + if is_id_start(c) { + current_pos += c.len_utf8(); + } else { + break; + } + } + while let Some(c) = chars.next() { + if is_id_continue(c) { + current_pos += c.len_utf8(); + } else { + break; + } + } + } + if current_pos == start_pos { None } else { Some(current_pos) } +} + fn is_valid_for_html_tag_name(c: char, is_empty: bool) -> bool { // https://spec.commonmark.org/0.30/#raw-html // @@ -158,7 +184,60 @@ fn extract_html_tag( } drop_tag(tags, tag_name, r, f); } else { - tags.push((tag_name, r)); + let mut is_self_closing = false; + let mut quote_pos = None; + if c != '>' { + let mut quote = None; + let mut after_eq = false; + for (i, c) in text[pos..].char_indices() { + if !c.is_whitespace() { + if let Some(q) = quote { + if c == q { + quote = None; + quote_pos = None; + after_eq = false; + } + } else if c == '>' { + break; + } else if c == '/' && !after_eq { + is_self_closing = true; + } else { + if is_self_closing { + is_self_closing = false; + } + if (c == '"' || c == '\'') && after_eq { + quote = Some(c); + quote_pos = Some(pos + i); + } else if c == '=' { + after_eq = true; + } + } + } else if quote.is_none() { + after_eq = false; + } + } + } + if let Some(quote_pos) = quote_pos { + let qr = Range { start: quote_pos, end: quote_pos }; + f( + &format!("unclosed quoted HTML attribute on tag `{}`", tag_name), + &qr, + false, + ); + } + if is_self_closing { + // https://html.spec.whatwg.org/#parse-error-non-void-html-element-start-tag-with-trailing-solidus + let valid = ALLOWED_UNCLOSED.contains(&&tag_name[..]) + || tags.iter().take(pos + 1).any(|(at, _)| { + let at = at.to_lowercase(); + at == "svg" || at == "math" + }); + if !valid { + f(&format!("invalid self-closing HTML tag `{}`", tag_name), &r, false); + } + } else { + tags.push((tag_name, r)); + } } } break; @@ -212,27 +291,74 @@ impl<'a, 'tcx> DocVisitor for InvalidHtmlTagsLinter<'a, 'tcx> { Some(sp) => sp, None => item.attr_span(tcx), }; - tcx.struct_span_lint_hir(crate::lint::INVALID_HTML_TAGS, hir_id, sp, |lint| { + tcx.struct_span_lint_hir(crate::lint::INVALID_HTML_TAGS, hir_id, sp, msg, |lint| { use rustc_lint_defs::Applicability; - let mut diag = lint.build(msg); // If a tag looks like `<this>`, it might actually be a generic. // We don't try to detect stuff `<like, this>` because that's not valid HTML, // and we don't try to detect stuff `<like this>` because that's not valid Rust. - if let Some(Some(generics_start)) = (is_open_tag - && dox[..range.end].ends_with('>')) + let mut generics_end = range.end; + if let Some(Some(mut generics_start)) = (is_open_tag + && dox[..generics_end].ends_with('>')) .then(|| extract_path_backwards(&dox, range.start)) { + while generics_start != 0 + && generics_end < dox.len() + && dox.as_bytes()[generics_start - 1] == b'<' + && dox.as_bytes()[generics_end] == b'>' + { + generics_end += 1; + generics_start -= 1; + if let Some(new_start) = extract_path_backwards(&dox, generics_start) { + generics_start = new_start; + } + if let Some(new_end) = extract_path_forward(&dox, generics_end) { + generics_end = new_end; + } + } + if let Some(new_end) = extract_path_forward(&dox, generics_end) { + generics_end = new_end; + } let generics_sp = match super::source_span_for_markdown_range( tcx, &dox, - &(generics_start..range.end), + &(generics_start..generics_end), &item.attrs, ) { Some(sp) => sp, None => item.attr_span(tcx), }; + // Sometimes, we only extract part of a path. For example, consider this: + // + // <[u32] as IntoIter<u32>>::Item + // ^^^^^ unclosed HTML tag `u32` + // + // We don't have any code for parsing fully-qualified trait paths. + // In theory, we could add it, but doing it correctly would require + // parsing the entire path grammar, which is problematic because of + // overlap between the path grammar and Markdown. + // + // The example above shows that ambiguity. Is `[u32]` intended to be an + // intra-doc link to the u32 primitive, or is it intended to be a slice? + // + // If the below conditional were removed, we would suggest this, which is + // not what the user probably wants. + // + // <[u32] as `IntoIter<u32>`>::Item + // + // We know that the user actually wants to wrap the whole thing in a code + // block, but the only reason we know that is because `u32` does not, in + // fact, implement IntoIter. If the example looks like this: + // + // <[Vec<i32>] as IntoIter<i32>::Item + // + // The ideal fix would be significantly different. + if (generics_start > 0 && dox.as_bytes()[generics_start - 1] == b'<') + || (generics_end < dox.len() && dox.as_bytes()[generics_end] == b'>') + { + return lint; + } // multipart form is chosen here because ``Vec<i32>`` would be confusing. - diag.multipart_suggestion( + lint.multipart_suggestion( "try marking as source code", vec![ (generics_sp.shrink_to_lo(), String::from("`")), @@ -241,7 +367,8 @@ impl<'a, 'tcx> DocVisitor for InvalidHtmlTagsLinter<'a, 'tcx> { Applicability::MaybeIncorrect, ); } - diag.emit() + + lint }); }; @@ -278,7 +405,7 @@ impl<'a, 'tcx> DocVisitor for InvalidHtmlTagsLinter<'a, 'tcx> { for (event, range) in p { match event { Event::Start(Tag::CodeBlock(_)) => in_code_block = true, - Event::Html(text) | Event::Text(text) if !in_code_block => { + Event::Html(text) if !in_code_block => { extract_tags(&mut tags, &text, range, &mut is_in_comment, &report_diag) } Event::End(Tag::CodeBlock(_)) => in_code_block = false, diff --git a/src/librustdoc/passes/propagate_doc_cfg.rs b/src/librustdoc/passes/propagate_doc_cfg.rs index 0c5d83655..de3a4b339 100644 --- a/src/librustdoc/passes/propagate_doc_cfg.rs +++ b/src/librustdoc/passes/propagate_doc_cfg.rs @@ -2,29 +2,74 @@ use std::sync::Arc; use crate::clean::cfg::Cfg; -use crate::clean::{Crate, Item}; +use crate::clean::inline::{load_attrs, merge_attrs}; +use crate::clean::{Crate, Item, ItemKind}; use crate::core::DocContext; use crate::fold::DocFolder; use crate::passes::Pass; +use rustc_hir::def_id::LocalDefId; + pub(crate) const PROPAGATE_DOC_CFG: Pass = Pass { name: "propagate-doc-cfg", run: propagate_doc_cfg, description: "propagates `#[doc(cfg(...))]` to child items", }; -pub(crate) fn propagate_doc_cfg(cr: Crate, _: &mut DocContext<'_>) -> Crate { - CfgPropagator { parent_cfg: None }.fold_crate(cr) +pub(crate) fn propagate_doc_cfg(cr: Crate, cx: &mut DocContext<'_>) -> Crate { + CfgPropagator { parent_cfg: None, parent: None, cx }.fold_crate(cr) } -struct CfgPropagator { +struct CfgPropagator<'a, 'tcx> { parent_cfg: Option<Arc<Cfg>>, + parent: Option<LocalDefId>, + cx: &'a mut DocContext<'tcx>, +} + +impl<'a, 'tcx> CfgPropagator<'a, 'tcx> { + // Some items need to merge their attributes with their parents' otherwise a few of them + // (mostly `cfg` ones) will be missing. + fn merge_with_parent_attributes(&mut self, item: &mut Item) { + let check_parent = match &*item.kind { + // impl blocks can be in different modules with different cfg and we need to get them + // as well. + ItemKind::ImplItem(_) => false, + kind if kind.is_non_assoc() => true, + _ => return, + }; + + let Some(def_id) = item.item_id.as_def_id().and_then(|def_id| def_id.as_local()) + else { return }; + + let hir = self.cx.tcx.hir(); + let hir_id = hir.local_def_id_to_hir_id(def_id); + + if check_parent { + let expected_parent = hir.get_parent_item(hir_id); + // If parents are different, it means that `item` is a reexport and we need + // to compute the actual `cfg` by iterating through its "real" parents. + if self.parent == Some(expected_parent.def_id) { + return; + } + } + + let mut attrs = Vec::new(); + for (parent_hir_id, _) in hir.parent_iter(hir_id) { + if let Some(def_id) = hir.opt_local_def_id(parent_hir_id) { + attrs.extend_from_slice(load_attrs(self.cx, def_id.to_def_id())); + } + } + let (_, cfg) = merge_attrs(self.cx, None, item.attrs.other_attrs.as_slice(), Some(&attrs)); + item.cfg = cfg; + } } -impl DocFolder for CfgPropagator { +impl<'a, 'tcx> DocFolder for CfgPropagator<'a, 'tcx> { fn fold_item(&mut self, mut item: Item) -> Option<Item> { let old_parent_cfg = self.parent_cfg.clone(); + self.merge_with_parent_attributes(&mut item); + let new_cfg = match (self.parent_cfg.take(), item.cfg.take()) { (None, None) => None, (Some(rc), None) | (None, Some(rc)) => Some(rc), @@ -37,8 +82,15 @@ impl DocFolder for CfgPropagator { self.parent_cfg = new_cfg.clone(); item.cfg = new_cfg; + let old_parent = + if let Some(def_id) = item.item_id.as_def_id().and_then(|def_id| def_id.as_local()) { + self.parent.replace(def_id) + } else { + self.parent.take() + }; let result = self.fold_item_recur(item); self.parent_cfg = old_parent_cfg; + self.parent = old_parent; Some(result) } diff --git a/src/librustdoc/passes/strip_hidden.rs b/src/librustdoc/passes/strip_hidden.rs index 533e2ce46..9914edf30 100644 --- a/src/librustdoc/passes/strip_hidden.rs +++ b/src/librustdoc/passes/strip_hidden.rs @@ -17,6 +17,7 @@ pub(crate) const STRIP_HIDDEN: Pass = Pass { /// Strip items marked `#[doc(hidden)]` pub(crate) fn strip_hidden(krate: clean::Crate, cx: &mut DocContext<'_>) -> clean::Crate { let mut retained = ItemIdSet::default(); + let is_json_output = cx.output_format.is_json() && !cx.show_coverage; // strip all #[doc(hidden)] items let krate = { @@ -25,7 +26,12 @@ pub(crate) fn strip_hidden(krate: clean::Crate, cx: &mut DocContext<'_>) -> clea }; // strip all impls referencing stripped items - let mut stripper = ImplStripper { retained: &retained, cache: &cx.cache }; + let mut stripper = ImplStripper { + retained: &retained, + cache: &cx.cache, + is_json_output, + document_private: cx.render_options.document_private, + }; stripper.fold_crate(krate) } diff --git a/src/librustdoc/passes/strip_private.rs b/src/librustdoc/passes/strip_private.rs index 9ba841a31..450f69e15 100644 --- a/src/librustdoc/passes/strip_private.rs +++ b/src/librustdoc/passes/strip_private.rs @@ -17,19 +17,25 @@ pub(crate) const STRIP_PRIVATE: Pass = Pass { pub(crate) fn strip_private(mut krate: clean::Crate, cx: &mut DocContext<'_>) -> clean::Crate { // This stripper collects all *retained* nodes. let mut retained = ItemIdSet::default(); + let is_json_output = cx.output_format.is_json() && !cx.show_coverage; // strip all private items { let mut stripper = Stripper { retained: &mut retained, - access_levels: &cx.cache.access_levels, + effective_visibilities: &cx.cache.effective_visibilities, update_retained: true, - is_json_output: cx.output_format.is_json() && !cx.show_coverage, + is_json_output, }; krate = ImportStripper.fold_crate(stripper.fold_crate(krate)); } // strip all impls referencing private items - let mut stripper = ImplStripper { retained: &retained, cache: &cx.cache }; + let mut stripper = ImplStripper { + retained: &retained, + cache: &cx.cache, + is_json_output, + document_private: cx.render_options.document_private, + }; stripper.fold_crate(krate) } diff --git a/src/librustdoc/passes/stripper.rs b/src/librustdoc/passes/stripper.rs index 0d419042a..0089ce63d 100644 --- a/src/librustdoc/passes/stripper.rs +++ b/src/librustdoc/passes/stripper.rs @@ -1,30 +1,34 @@ //! A collection of utility functions for the `strip_*` passes. use rustc_hir::def_id::DefId; -use rustc_middle::middle::privacy::AccessLevels; +use rustc_middle::middle::privacy::EffectiveVisibilities; +use rustc_span::symbol::sym; + use std::mem; -use crate::clean::{self, Item, ItemId, ItemIdSet}; +use crate::clean::{self, Item, ItemId, ItemIdSet, NestedAttributesExt}; use crate::fold::{strip_item, DocFolder}; use crate::formats::cache::Cache; pub(crate) struct Stripper<'a> { pub(crate) retained: &'a mut ItemIdSet, - pub(crate) access_levels: &'a AccessLevels<DefId>, + pub(crate) effective_visibilities: &'a EffectiveVisibilities<DefId>, pub(crate) update_retained: bool, pub(crate) is_json_output: bool, } -impl<'a> Stripper<'a> { - // We need to handle this differently for the JSON output because some non exported items could - // be used in public API. And so, we need these items as well. `is_exported` only checks if they - // are in the public API, which is not enough. - #[inline] - fn is_item_reachable(&self, item_id: ItemId) -> bool { - if self.is_json_output { - self.access_levels.is_reachable(item_id.expect_def_id()) - } else { - self.access_levels.is_exported(item_id.expect_def_id()) - } +// We need to handle this differently for the JSON output because some non exported items could +// be used in public API. And so, we need these items as well. `is_exported` only checks if they +// are in the public API, which is not enough. +#[inline] +fn is_item_reachable( + is_json_output: bool, + effective_visibilities: &EffectiveVisibilities<DefId>, + item_id: ItemId, +) -> bool { + if is_json_output { + effective_visibilities.is_reachable(item_id.expect_def_id()) + } else { + effective_visibilities.is_exported(item_id.expect_def_id()) } } @@ -61,7 +65,9 @@ impl<'a> DocFolder for Stripper<'a> { | clean::MacroItem(..) | clean::ForeignTypeItem => { let item_id = i.item_id; - if item_id.is_local() && !self.is_item_reachable(item_id) { + if item_id.is_local() + && !is_item_reachable(self.is_json_output, self.effective_visibilities, item_id) + { debug!("Stripper: stripping {:?} {:?}", i.type_(), i.name); return None; } @@ -84,7 +90,17 @@ impl<'a> DocFolder for Stripper<'a> { } // handled in the `strip-priv-imports` pass - clean::ExternCrateItem { .. } | clean::ImportItem(..) => {} + clean::ExternCrateItem { .. } => {} + clean::ImportItem(ref imp) => { + // Because json doesn't inline imports from private modules, we need to mark + // the imported item as retained so it's impls won't be stripped. + // + // FIXME: Is it necessary to check for json output here: See + // https://github.com/rust-lang/rust/pull/100325#discussion_r941495215 + if let Some(did) = imp.source.did && self.is_json_output { + self.retained.insert(did.into()); + } + } clean::ImplItem(..) => {} @@ -133,6 +149,24 @@ impl<'a> DocFolder for Stripper<'a> { pub(crate) struct ImplStripper<'a> { pub(crate) retained: &'a ItemIdSet, pub(crate) cache: &'a Cache, + pub(crate) is_json_output: bool, + pub(crate) document_private: bool, +} + +impl<'a> ImplStripper<'a> { + #[inline] + fn should_keep_impl(&self, item: &Item, for_def_id: DefId) -> bool { + if !for_def_id.is_local() || self.retained.contains(&for_def_id.into()) { + true + } else if self.is_json_output { + // If the "for" item is exported and the impl block isn't `#[doc(hidden)]`, then we + // need to keep it. + self.cache.effective_visibilities.is_exported(for_def_id) + && !item.attrs.lists(sym::doc).has_word(sym::hidden) + } else { + false + } + } } impl<'a> DocFolder for ImplStripper<'a> { @@ -140,18 +174,39 @@ impl<'a> DocFolder for ImplStripper<'a> { if let clean::ImplItem(ref imp) = *i.kind { // Impl blocks can be skipped if they are: empty; not a trait impl; and have no // documentation. - if imp.trait_.is_none() && imp.items.is_empty() && i.doc_value().is_none() { - return None; + // + // There is one special case: if the impl block contains only private items. + if imp.trait_.is_none() { + // If the only items present are private ones and we're not rendering private items, + // we don't document it. + if !imp.items.is_empty() + && !self.document_private + && imp.items.iter().all(|i| { + let item_id = i.item_id; + item_id.is_local() + && !is_item_reachable( + self.is_json_output, + &self.cache.effective_visibilities, + item_id, + ) + }) + { + return None; + } else if imp.items.is_empty() && i.doc_value().is_none() { + return None; + } } + // Because we don't inline in `maybe_inline_local` if the output format is JSON, + // we need to make a special check for JSON output: we want to keep it unless it has + // a `#[doc(hidden)]` attribute if the `for_` type is exported. if let Some(did) = imp.for_.def_id(self.cache) { - if did.is_local() && !imp.for_.is_assoc_ty() && !self.retained.contains(&did.into()) - { + if !imp.for_.is_assoc_ty() && !self.should_keep_impl(&i, did) { debug!("ImplStripper: impl item for stripped type; removing"); return None; } } if let Some(did) = imp.trait_.as_ref().map(|t| t.def_id()) { - if did.is_local() && !self.retained.contains(&did.into()) { + if !self.should_keep_impl(&i, did) { debug!("ImplStripper: impl item for stripped trait; removing"); return None; } @@ -159,7 +214,7 @@ impl<'a> DocFolder for ImplStripper<'a> { if let Some(generics) = imp.trait_.as_ref().and_then(|t| t.generics()) { for typaram in generics { if let Some(did) = typaram.def_id(self.cache) { - if did.is_local() && !self.retained.contains(&did.into()) { + if !self.should_keep_impl(&i, did) { debug!( "ImplStripper: stripped item in trait's generics; removing impl" ); |