blob: ac221743cfd641b6a5dce9c4511647eb1a763f08 [file] [log] [blame] [edit]
use clippy_config::Conf;
use clippy_utils::diagnostics::span_lint;
use clippy_utils::{is_doc_hidden, is_from_proc_macro};
use rustc_hir::attrs::AttributeKind;
use rustc_hir::def_id::LocalDefId;
use rustc_hir::{
AttrArgs, Attribute, Body, BodyId, FieldDef, HirId, ImplItem, Item, ItemKind, Node, TraitItem, Variant,
};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::middle::privacy::Level;
use rustc_middle::ty::Visibility;
use rustc_session::impl_lint_pass;
use rustc_span::def_id::CRATE_DEF_ID;
use rustc_span::sym;
use rustc_span::symbol::kw;
declare_clippy_lint! {
/// ### What it does
/// Warns if there is missing documentation for any private documentable item.
///
/// ### Why restrict this?
/// Doc is good. *rustc* has a `MISSING_DOCS`
/// allowed-by-default lint for
/// public members, but has no way to enforce documentation of private items.
/// This lint fixes that.
#[clippy::version = "pre 1.29.0"]
pub MISSING_DOCS_IN_PRIVATE_ITEMS,
restriction,
"detects missing documentation for private members"
}
pub struct MissingDoc {
/// Whether to **only** check for missing documentation in items visible within the current
/// crate. For example, `pub(crate)` items.
crate_items_only: bool,
/// Whether to allow fields starting with an underscore to skip documentation requirements
allow_unused: bool,
/// The current number of modules since the crate root.
module_depth: u32,
macro_module_depth: u32,
/// The current level of the attribute stack.
attr_depth: u32,
/// What `attr_depth` level the first `doc(hidden)` attribute was seen. This is zero if the
/// attribute hasn't been seen.
doc_hidden_depth: u32,
/// What `attr_depth` level the first `automatically_derived` attribute was seen. This is zero
/// if the attribute hasn't been seen.
automatically_derived_depth: u32,
/// The id of the first body we've seen.
in_body: Option<BodyId>,
/// The module/crate id an item must be visible at to be linted.
require_visibility_at: Option<LocalDefId>,
}
impl MissingDoc {
pub fn new(conf: &'static Conf) -> Self {
Self {
crate_items_only: conf.missing_docs_in_crate_items,
allow_unused: conf.missing_docs_allow_unused,
module_depth: 0,
macro_module_depth: 0,
attr_depth: 0,
doc_hidden_depth: 0,
automatically_derived_depth: 0,
in_body: None,
require_visibility_at: None,
}
}
fn is_missing_docs(&self, cx: &LateContext<'_>, def_id: LocalDefId, hir_id: HirId) -> bool {
if cx.tcx.sess.opts.test {
return false;
}
match cx.effective_visibilities.effective_vis(def_id) {
None if self.require_visibility_at.is_some() => return false,
None if self.crate_items_only && self.module_depth != 0 => return false,
// `missing_docs` lint uses `Reexported` because rustdoc doesn't render documentation
// for items without a reachable path.
Some(vis) if vis.is_public_at_level(Level::Reexported) => return false,
Some(vis) => {
if self.crate_items_only {
// Use the `Reachable` level since rustdoc will be able to render the documentation
// when building private docs.
let vis = vis.at_level(Level::Reachable);
if !(vis.is_public() || matches!(vis, Visibility::Restricted(id) if id.is_top_level_module())) {
return false;
}
} else if let Some(id) = self.require_visibility_at
&& !vis.at_level(Level::Reexported).is_accessible_from(id, cx.tcx)
{
return false;
}
},
None => {},
}
!cx.tcx.hir_attrs(hir_id).iter().any(is_doc_attr)
}
}
impl_lint_pass!(MissingDoc => [MISSING_DOCS_IN_PRIVATE_ITEMS]);
impl<'tcx> LateLintPass<'tcx> for MissingDoc {
fn check_attributes(&mut self, _: &LateContext<'tcx>, attrs: &'tcx [Attribute]) {
self.attr_depth += 1;
if self.doc_hidden_depth == 0 && is_doc_hidden(attrs) {
self.doc_hidden_depth = self.attr_depth;
}
}
fn check_attributes_post(&mut self, _: &LateContext<'tcx>, _: &'tcx [Attribute]) {
self.attr_depth -= 1;
if self.attr_depth < self.doc_hidden_depth {
self.doc_hidden_depth = 0;
}
if self.attr_depth < self.automatically_derived_depth {
self.automatically_derived_depth = 0;
}
}
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) {
if self.doc_hidden_depth != 0 || self.automatically_derived_depth != 0 || self.in_body.is_some() {
return;
}
let span = match item.kind {
// ignore main()
ItemKind::Fn { ident, .. }
if ident.name == sym::main && cx.tcx.local_parent(item.owner_id.def_id) == CRATE_DEF_ID =>
{
return;
},
ItemKind::Const(ident, ..) if ident.name == kw::Underscore => return,
ItemKind::Impl { .. } => {
if cx.tcx.is_automatically_derived(item.owner_id.def_id.to_def_id()) {
self.automatically_derived_depth = self.attr_depth;
}
return;
},
ItemKind::ExternCrate(..)
| ItemKind::ForeignMod { .. }
| ItemKind::GlobalAsm { .. }
| ItemKind::Use(..) => return,
ItemKind::Mod(ident, ..) => {
if item.span.from_expansion() && item.span.eq_ctxt(ident.span) {
self.module_depth += 1;
self.require_visibility_at = cx.tcx.opt_local_parent(item.owner_id.def_id);
self.macro_module_depth = self.module_depth;
return;
}
ident.span
},
ItemKind::Const(ident, ..)
| ItemKind::Enum(ident, ..)
| ItemKind::Fn { ident, .. }
| ItemKind::Macro(ident, ..)
| ItemKind::Static(_, ident, ..)
| ItemKind::Struct(ident, ..)
| ItemKind::Trait(_, _, _, ident, ..)
| ItemKind::TraitAlias(_, ident, ..)
| ItemKind::TyAlias(ident, ..)
| ItemKind::Union(ident, ..) => ident.span,
};
if !item.span.from_expansion()
&& self.is_missing_docs(cx, item.owner_id.def_id, item.hir_id())
&& !is_from_proc_macro(cx, item)
{
let (article, desc) = cx.tcx.article_and_description(item.owner_id.to_def_id());
span_lint(
cx,
MISSING_DOCS_IN_PRIVATE_ITEMS,
span,
format!("missing documentation for {article} {desc}"),
);
}
if matches!(item.kind, ItemKind::Mod(..)) {
self.module_depth += 1;
}
}
fn check_item_post(&mut self, _: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
if matches!(item.kind, ItemKind::Mod(..))
&& self.doc_hidden_depth == 0
&& self.automatically_derived_depth == 0
&& self.in_body.is_none()
{
self.module_depth -= 1;
if self.module_depth < self.macro_module_depth {
self.require_visibility_at = None;
}
}
}
fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'_>) {
if self.doc_hidden_depth == 0
&& self.automatically_derived_depth == 0
&& self.in_body.is_none()
&& !item.span.from_expansion()
&& self.is_missing_docs(cx, item.owner_id.def_id, item.hir_id())
&& !is_from_proc_macro(cx, item)
{
let (article, desc) = cx.tcx.article_and_description(item.owner_id.to_def_id());
span_lint(
cx,
MISSING_DOCS_IN_PRIVATE_ITEMS,
item.ident.span,
format!("missing documentation for {article} {desc}"),
);
}
}
fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx ImplItem<'_>) {
if self.doc_hidden_depth == 0
&& self.automatically_derived_depth == 0
&& self.in_body.is_none()
&& let Node::Item(parent) = cx.tcx.parent_hir_node(item.hir_id())
&& let ItemKind::Impl(impl_) = parent.kind
&& impl_.of_trait.is_none()
&& !item.span.from_expansion()
&& self.is_missing_docs(cx, item.owner_id.def_id, item.hir_id())
&& !is_from_proc_macro(cx, item)
{
let (article, desc) = cx.tcx.article_and_description(item.owner_id.to_def_id());
span_lint(
cx,
MISSING_DOCS_IN_PRIVATE_ITEMS,
item.ident.span,
format!("missing documentation for {article} {desc}"),
);
}
}
fn check_body(&mut self, _: &LateContext<'tcx>, body: &Body<'tcx>) {
if self.doc_hidden_depth == 0 && self.automatically_derived_depth == 0 && self.in_body.is_none() {
self.in_body = Some(body.id());
}
}
fn check_body_post(&mut self, _: &LateContext<'tcx>, body: &Body<'tcx>) {
if self.in_body == Some(body.id()) {
self.in_body = None;
}
}
fn check_field_def(&mut self, cx: &LateContext<'tcx>, field: &'tcx FieldDef<'_>) {
if self.doc_hidden_depth == 0
&& self.automatically_derived_depth == 0
&& self.in_body.is_none()
&& !field.is_positional()
&& !field.span.from_expansion()
&& !(self.allow_unused && field.ident.name.as_str().starts_with('_'))
&& self.is_missing_docs(cx, field.def_id, field.hir_id)
&& !is_from_proc_macro(cx, field)
{
span_lint(
cx,
MISSING_DOCS_IN_PRIVATE_ITEMS,
field.ident.span,
"missing documentation for a field",
);
}
}
fn check_variant(&mut self, cx: &LateContext<'tcx>, variant: &'tcx Variant<'_>) {
if self.doc_hidden_depth == 0
&& self.automatically_derived_depth == 0
&& self.in_body.is_none()
&& !variant.span.from_expansion()
&& self.is_missing_docs(cx, variant.def_id, variant.hir_id)
&& !is_from_proc_macro(cx, variant)
{
span_lint(
cx,
MISSING_DOCS_IN_PRIVATE_ITEMS,
variant.ident.span,
"missing documentation for a variant",
);
}
}
}
fn is_doc_attr(attr: &Attribute) -> bool {
match attr {
Attribute::Parsed(AttributeKind::DocComment { .. }) => true,
Attribute::Unparsed(attr)
if let [ident] = &*attr.path.segments
&& ident.name == sym::doc =>
{
matches!(attr.args, AttrArgs::Eq { .. })
},
_ => false,
}
}