Add new lint doc_include_without_cfg (#13625)

It's becoming more and more common to see people including markdown
files in their code using `doc = include_str!("...")`, which is great.
However, often there is no condition on this include, which is not great
because it slows down compilation and might trigger recompilation if
these files are updated.

This lint aims at fixing this situation.

changelog: Add new lint `doc_include_without_cfg`
This commit is contained in:
Alejandra González 2024-11-21 21:48:57 +00:00 committed by GitHub
commit 8298da72e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 174 additions and 1 deletions

View file

@ -135,6 +135,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
crate::disallowed_names::DISALLOWED_NAMES_INFO,
crate::disallowed_script_idents::DISALLOWED_SCRIPT_IDENTS_INFO,
crate::disallowed_types::DISALLOWED_TYPES_INFO,
crate::doc::DOC_INCLUDE_WITHOUT_CFG_INFO,
crate::doc::DOC_LAZY_CONTINUATION_INFO,
crate::doc::DOC_LINK_WITH_QUOTES_INFO,
crate::doc::DOC_MARKDOWN_INFO,

View file

@ -0,0 +1,45 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::snippet_opt;
use rustc_ast::{AttrArgs, AttrArgsEq, AttrKind, AttrStyle, Attribute};
use rustc_errors::Applicability;
use rustc_lint::LateContext;
use rustc_span::sym;
use super::DOC_INCLUDE_WITHOUT_CFG;
pub fn check(cx: &LateContext<'_>, attrs: &[Attribute]) {
for attr in attrs {
if !attr.span.from_expansion()
&& let AttrKind::Normal(ref normal) = attr.kind
&& normal.item.path == sym::doc
&& let AttrArgs::Eq(_, AttrArgsEq::Hir(ref meta)) = normal.item.args
&& !attr.span.contains(meta.span)
// Since the `include_str` is already expanded at this point, we can only take the
// whole attribute snippet and then modify for our suggestion.
&& let Some(snippet) = snippet_opt(cx, attr.span)
// We cannot remove this because a `#[doc = include_str!("...")]` attribute can occupy
// several lines.
&& let Some(start) = snippet.find('[')
&& let Some(end) = snippet.rfind(']')
&& let snippet = &snippet[start + 1..end]
// We check that the expansion actually comes from `include_str!` and not just from
// another macro.
&& let Some(sub_snippet) = snippet.trim().strip_prefix("doc")
&& let Some(sub_snippet) = sub_snippet.trim().strip_prefix("=")
&& sub_snippet.trim().starts_with("include_str!")
{
span_lint_and_sugg(
cx,
DOC_INCLUDE_WITHOUT_CFG,
attr.span,
"included a file in documentation unconditionally",
"use `cfg_attr(doc, doc = \"...\")`",
format!(
"#{}[cfg_attr(doc, {snippet})]",
if attr.style == AttrStyle::Inner { "!" } else { "" }
),
Applicability::MachineApplicable,
);
}
}
}

View file

@ -1,3 +1,5 @@
#![allow(clippy::lint_without_lint_pass)]
mod lazy_continuation;
mod too_long_first_doc_paragraph;
@ -33,6 +35,7 @@ use std::ops::Range;
use url::Url;
mod empty_line_after;
mod include_in_doc_without_cfg;
mod link_with_quotes;
mod markdown;
mod missing_headers;
@ -532,6 +535,29 @@ declare_clippy_lint! {
"empty line after doc comments"
}
declare_clippy_lint! {
/// ### What it does
/// Checks if included files in doc comments are included only for `cfg(doc)`.
///
/// ### Why is this bad?
/// These files are not useful for compilation but will still be included.
/// Also, if any of these non-source code file is updated, it will trigger a
/// recompilation.
///
/// ### Example
/// ```ignore
/// #![doc = include_str!("some_file.md")]
/// ```
/// Use instead:
/// ```no_run
/// #![cfg_attr(doc, doc = include_str!("some_file.md"))]
/// ```
#[clippy::version = "1.84.0"]
pub DOC_INCLUDE_WITHOUT_CFG,
pedantic,
"check if files included in documentation are behind `cfg(doc)`"
}
pub struct Documentation {
valid_idents: FxHashSet<String>,
check_private_items: bool,
@ -561,6 +587,7 @@ impl_lint_pass!(Documentation => [
EMPTY_LINE_AFTER_OUTER_ATTR,
EMPTY_LINE_AFTER_DOC_COMMENTS,
TOO_LONG_FIRST_DOC_PARAGRAPH,
DOC_INCLUDE_WITHOUT_CFG,
]);
impl<'tcx> LateLintPass<'tcx> for Documentation {
@ -690,6 +717,7 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
Some(("fake".into(), "fake".into()))
}
include_in_doc_without_cfg::check(cx, attrs);
if suspicious_doc_comments::check(cx, attrs) || empty_line_after::check(cx, attrs) || is_doc_hidden(attrs) {
return None;
}