Skip to content

add new rustdoc::hidden_intra_doc_links lint #144750

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 69 additions & 10 deletions src/librustdoc/html/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use std::cmp::Ordering;
use std::fmt::{self, Display, Write};
use std::iter::{self, once};
use std::ops::ControlFlow;
use std::slice;

use itertools::{Either, Itertools};
Expand All @@ -21,7 +22,7 @@ use rustc_hir::def::DefKind;
use rustc_hir::def_id::{DefId, LOCAL_CRATE};
use rustc_hir::{ConstStability, StabilityLevel, StableSince};
use rustc_metadata::creader::{CStore, LoadedMacro};
use rustc_middle::ty::{self, TyCtxt, TypingMode};
use rustc_middle::ty::{self, ImplSubject, TyCtxt, TypingMode};
use rustc_span::symbol::kw;
use rustc_span::{Symbol, sym};
use tracing::{debug, trace};
Expand Down Expand Up @@ -483,22 +484,80 @@ fn generate_item_def_id_path(

/// Checks if the given defid refers to an item that is unnamable, such as one defined in a const block.
fn is_unnamable(tcx: TyCtxt<'_>, did: DefId) -> bool {
traverse_parent(tcx, did, false, true, &|_| ControlFlow::Continue(()))
}

pub(crate) fn traverse_parent(
tcx: TyCtxt<'_>,
did: DefId,
if_noparent: bool,
if_unnamable: bool,
callback: &dyn Fn(DefId) -> ControlFlow<bool>,
) -> bool {
let mut cur_did = did;
while let Some(parent) = tcx.opt_parent(cur_did) {
if let ControlFlow::Break(x) = callback(cur_did) {
return x;
}
match tcx.def_kind(parent) {
// items defined in these can be linked to, as long as they are visible
DefKind::Mod | DefKind::ForeignMod => cur_did = parent,
// items in impls can be linked to,
// as long as we can link to the item the impl is on.
// since associated traits are not a thing,
// it should not be possible to refer to an impl item if
// the base type is not namable.
DefKind::Impl { .. } => return false,
DefKind::Mod | DefKind::ForeignMod | DefKind::Trait => cur_did = parent,
// this serves both to future-proof is_unnamable for associated trait aliases,
// and traverses upwards to see if the trait or type is doc(hidden)
DefKind::Impl { .. } => {
match tcx.impl_subject(parent).as_ref().skip_binder() {
ImplSubject::Trait(trait_ref) => {
// first, check if the trait fufills the criteria
if traverse_parent(
tcx,
trait_ref.def_id,
if_noparent,
if_unnamable,
callback,
) {
return true;
}

// for trait impls on local types, we also check if the type matches the criteria
if let Some(hir::Node::Item(hir::Item {
kind: hir::ItemKind::Impl(hir::Impl { self_ty, .. }),
..
})) = tcx.hir_get_if_local(parent)
&& let hir::Ty {
kind:
hir::TyKind::Path(hir::QPath::Resolved(
_,
hir::Path {
res: hir::def::Res::Def(_, self_ty_def_id),
..
},
)),
..
} = self_ty.peel_refs()
/*&& let Some(local_def_id) = parent.as_local()
&& let Some(self_ty_def_id) = tcx.typeck(local_def_id).type_dependent_def_id(self_ty.hir_id)*/
{
cur_did = *self_ty_def_id;
continue;
}
return if_noparent;
}
ImplSubject::Inherent(ty) => {
if let Some(def_id) =
rustc_middle::ty::print::characteristic_def_id_of_type(ty.peel_refs())
{
cur_did = def_id;
} else {
return if_noparent;
}
}
}
}
// everything else does not have docs generated for it
_ => return true,
_ => return if_unnamable,
}
}
return false;
return if_noparent;
}

fn to_module_fqp(shortty: ItemType, fqp: &[Symbol]) -> &[Symbol] {
Expand Down
12 changes: 12 additions & 0 deletions src/librustdoc/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ declare_rustdoc_lint! {
"linking from a public item to a private one"
}

declare_rustdoc_lint! {
/// This is a subset of `broken_intra_doc_links` that warns when linking from
/// a non-hidden item to a hidden one. This is a `rustdoc` only lint, see the
/// documentation in the [rustdoc book].
///
/// [rustdoc book]: ../../../rustdoc/lints.html#hidden_intra_doc_links
HIDDEN_INTRA_DOC_LINKS,
Warn,
"linking from a non-hidden item to a hidden one"
}

declare_rustdoc_lint! {
/// The `invalid_codeblock_attributes` lint detects code block attributes
/// in documentation examples that have potentially mis-typed values. This
Expand Down Expand Up @@ -199,6 +210,7 @@ declare_rustdoc_lint! {
pub(crate) static RUSTDOC_LINTS: Lazy<Vec<&'static Lint>> = Lazy::new(|| {
vec![
BROKEN_INTRA_DOC_LINKS,
HIDDEN_INTRA_DOC_LINKS,
PRIVATE_INTRA_DOC_LINKS,
MISSING_DOC_CODE_EXAMPLES,
PRIVATE_DOC_TESTS,
Expand Down
50 changes: 40 additions & 10 deletions src/librustdoc/passes/collect_intra_doc_links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use std::borrow::Cow;
use std::fmt::Display;
use std::mem;
use std::ops::Range;
use std::ops::{ControlFlow, Range};

use pulldown_cmark::LinkType;
use rustc_ast::util::comments::may_have_doc_links;
Expand Down Expand Up @@ -34,7 +34,7 @@ use crate::clean::utils::find_nearest_parent_module;
use crate::clean::{self, Crate, Item, ItemId, ItemLink, PrimitiveType};
use crate::core::DocContext;
use crate::html::markdown::{MarkdownLink, MarkdownLinkRange, markdown_links};
use crate::lint::{BROKEN_INTRA_DOC_LINKS, PRIVATE_INTRA_DOC_LINKS};
use crate::lint::{BROKEN_INTRA_DOC_LINKS, HIDDEN_INTRA_DOC_LINKS, PRIVATE_INTRA_DOC_LINKS};
use crate::passes::Pass;
use crate::visit::DocVisitor;

Expand Down Expand Up @@ -1341,13 +1341,19 @@ impl LinkCollector<'_, '_> {
}
}

let src_def_id = diag_info.item.item_id.expect_def_id();
// item can be non-local e.g. when using `#[rustc_doc_primitive = "pointer"]`
if let Some(dst_id) = id.as_local()
&& let Some(src_id) = diag_info.item.item_id.expect_def_id().as_local()
&& let Some(src_id) = src_def_id.as_local()
&& 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);
privacy_error(self.cx, diag_info, path_str, PrivacyErrorKind::Private);
}

if is_effectively_hidden(self.cx.tcx, id) && !is_effectively_hidden(self.cx.tcx, src_def_id)
{
privacy_error(self.cx, diag_info, path_str, PrivacyErrorKind::Hidden);
}

Some(())
Expand Down Expand Up @@ -1591,6 +1597,13 @@ fn range_between_backticks(ori_link_range: &MarkdownLinkRange, dox: &str) -> Mar
)
}

/// Is this item or any of its ancestors doc(hidden)?
fn is_effectively_hidden(tcx: TyCtxt<'_>, id: DefId) -> bool {
crate::html::format::traverse_parent(tcx, id, false, false, &|id| {
if tcx.is_doc_hidden(id) { ControlFlow::Break(true) } else { ControlFlow::Continue(()) }
})
}

/// Returns true if we should ignore `link` due to it being unlikely
/// that it is an intra-doc link. `link` should still have disambiguators
/// if there were any.
Expand Down Expand Up @@ -2334,8 +2347,20 @@ fn suggest_disambiguator(
}
}

#[derive(Copy, Clone)]
enum PrivacyErrorKind {
Private,
Hidden,
}

/// Report a link from a public item to a private one.
fn privacy_error(cx: &DocContext<'_>, diag_info: &DiagnosticInfo<'_>, path_str: &str) {
fn privacy_error(
cx: &DocContext<'_>,
diag_info: &DiagnosticInfo<'_>,
path_str: &str,
kind: PrivacyErrorKind,
) {
use PrivacyErrorKind::*;
let sym;
let item_name = match diag_info.item.name {
Some(name) => {
Expand All @@ -2344,17 +2369,22 @@ fn privacy_error(cx: &DocContext<'_>, diag_info: &DiagnosticInfo<'_>, path_str:
}
None => "<unknown>",
};
let msg = format!("public documentation for `{item_name}` links to private item `{path_str}`");
let (public, private, flag, lint) = match kind {
Private => ("public", "private", "--document-private-items", PRIVATE_INTRA_DOC_LINKS),
Hidden => ("non-hidden", "hidden", "--document-hidden-items", HIDDEN_INTRA_DOC_LINKS),
};
let msg =
format!("{public} documentation for `{item_name}` links to {private} item `{path_str}`");

report_diagnostic(cx.tcx, PRIVATE_INTRA_DOC_LINKS, msg, diag_info, |diag, sp, _link_range| {
report_diagnostic(cx.tcx, lint, msg, diag_info, |diag, sp, _link_range| {
if let Some(sp) = sp {
diag.span_label(sp, "this item is private");
diag.span_label(sp, format!("this item is {private}"));
}

let note_msg = if cx.render_options.document_private {
"this link resolves only because you passed `--document-private-items`, but will break without"
format!("this link resolves only because you passed `{flag}`, but will break without")
} else {
"this link will resolve properly if you pass `--document-private-items`"
format!("this link will resolve properly if you pass `{flag}`")
};
diag.note(note_msg);
});
Expand Down
9 changes: 9 additions & 0 deletions tests/rustdoc-ui/intra-doc/auxiliary/hidden.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#[doc(hidden)]
pub fn public_hidden_fn() {}

#[doc(hidden)]
fn private_hidden_fn() {}

pub fn public_non_hidden_fn() {}

fn private_non_hidden_fn() {}
92 changes: 92 additions & 0 deletions tests/rustdoc-ui/intra-doc/link-to-hidden-144664.doc-both.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
error: non-hidden documentation for `link_to_hidden_144664` links to hidden item `crate::local_public_hidden_fn`
--> $DIR/link-to-hidden-144664.rs:14:8
|
LL | //! * [crate::local_public_hidden_fn]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this item is hidden
|
= note: this link resolves only because you passed `--document-hidden-items`, but will break without
note: the lint level is defined here
--> $DIR/link-to-hidden-144664.rs:10:9
|
LL | #![deny(rustdoc::hidden_intra_doc_links)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: public documentation for `link_to_hidden_144664` links to private item `crate::local_private_hidden_fn`
--> $DIR/link-to-hidden-144664.rs:18:8
|
LL | //! * [crate::local_private_hidden_fn]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this item is private
|
= note: this link resolves only because you passed `--document-private-items`, but will break without
note: the lint level is defined here
--> $DIR/link-to-hidden-144664.rs:9:9
|
LL | #![deny(rustdoc::private_intra_doc_links)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: non-hidden documentation for `link_to_hidden_144664` links to hidden item `crate::local_private_hidden_fn`
--> $DIR/link-to-hidden-144664.rs:18:8
|
LL | //! * [crate::local_private_hidden_fn]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this item is hidden
|
= note: this link resolves only because you passed `--document-hidden-items`, but will break without

error: public documentation for `link_to_hidden_144664` links to private item `crate::local_private_non_hidden_fn`
--> $DIR/link-to-hidden-144664.rs:22:8
|
LL | //! * [crate::local_private_non_hidden_fn]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this item is private
|
= note: this link resolves only because you passed `--document-private-items`, but will break without

error: non-hidden documentation for `link_to_hidden_144664` links to hidden item `nonlocal::public_hidden_fn`
--> $DIR/link-to-hidden-144664.rs:24:8
|
LL | //! * [nonlocal::public_hidden_fn]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ this item is hidden
|
= note: this link resolves only because you passed `--document-hidden-items`, but will break without

error: unresolved link to `nonlocal::private_hidden_fn`
--> $DIR/link-to-hidden-144664.rs:28:8
|
LL | //! * [nonlocal::private_hidden_fn]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ no item named `private_hidden_fn` in module `hidden`
|
note: the lint level is defined here
--> $DIR/link-to-hidden-144664.rs:11:9
|
LL | #![deny(rustdoc::broken_intra_doc_links)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: unresolved link to `nonlocal::private_non_hidden_fn`
--> $DIR/link-to-hidden-144664.rs:30:8
|
LL | //! * [nonlocal::private_non_hidden_fn]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no item named `private_non_hidden_fn` in module `hidden`

error: unresolved link to `crate::hidden_mod::a`
--> $DIR/link-to-hidden-144664.rs:32:8
|
LL | //! * [crate::hidden_mod::a]
| ^^^^^^^^^^^^^^^^^^^^ no item named `a` in module `hidden_mod`

error: non-hidden documentation for `link_to_hidden_144664` links to hidden item `crate::hidden_mod::b`
--> $DIR/link-to-hidden-144664.rs:34:8
|
LL | //! * [crate::hidden_mod::b]
| ^^^^^^^^^^^^^^^^^^^^ this item is hidden
|
= note: this link resolves only because you passed `--document-hidden-items`, but will break without

error: non-hidden documentation for `link_to_hidden_144664` links to hidden item `crate::hidden_mod::c`
--> $DIR/link-to-hidden-144664.rs:36:8
|
LL | //! * [crate::hidden_mod::c]
| ^^^^^^^^^^^^^^^^^^^^ this item is hidden
|
= note: this link resolves only because you passed `--document-hidden-items`, but will break without

error: aborting due to 10 previous errors

Loading
Loading