Skip to content

Split doc.rs up into a subdirectory #11801

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

Merged
merged 1 commit into from
Nov 20, 2023
Merged
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
20 changes: 20 additions & 0 deletions clippy_lints/src/doc/link_with_quotes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use std::ops::Range;

use clippy_utils::diagnostics::span_lint;
use rustc_lint::LateContext;

use super::{Fragments, DOC_LINK_WITH_QUOTES};

pub fn check(cx: &LateContext<'_>, trimmed_text: &str, range: Range<usize>, fragments: Fragments<'_>) {
if ((trimmed_text.starts_with('\'') && trimmed_text.ends_with('\''))
|| (trimmed_text.starts_with('"') && trimmed_text.ends_with('"')))
&& let Some(span) = fragments.span(cx, range)
{
span_lint(
cx,
DOC_LINK_WITH_QUOTES,
span,
"possible intra-doc link using quotes instead of backticks",
);
}
}
109 changes: 109 additions & 0 deletions clippy_lints/src/doc/markdown.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
use clippy_utils::source::snippet_with_applicability;
use rustc_data_structures::fx::FxHashSet;
use rustc_errors::{Applicability, SuggestionStyle};
use rustc_lint::LateContext;
use rustc_span::{BytePos, Pos, Span};
use url::Url;

use crate::doc::DOC_MARKDOWN;

pub fn check(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, text: &str, span: Span) {
for word in text.split(|c: char| c.is_whitespace() || c == '\'') {
// Trim punctuation as in `some comment (see foo::bar).`
// ^^
// Or even as in `_foo bar_` which is emphasized. Also preserve `::` as a prefix/suffix.
let mut word = word.trim_matches(|c: char| !c.is_alphanumeric() && c != ':');

// Remove leading or trailing single `:` which may be part of a sentence.
if word.starts_with(':') && !word.starts_with("::") {
word = word.trim_start_matches(':');
}
if word.ends_with(':') && !word.ends_with("::") {
word = word.trim_end_matches(':');
}

if valid_idents.contains(word) || word.chars().all(|c| c == ':') {
continue;
}

// Adjust for the current word
let offset = word.as_ptr() as usize - text.as_ptr() as usize;
let span = Span::new(
span.lo() + BytePos::from_usize(offset),
span.lo() + BytePos::from_usize(offset + word.len()),
span.ctxt(),
span.parent(),
);

check_word(cx, word, span);
}
}

fn check_word(cx: &LateContext<'_>, word: &str, span: Span) {
/// Checks if a string is upper-camel-case, i.e., starts with an uppercase and
/// contains at least two uppercase letters (`Clippy` is ok) and one lower-case
/// letter (`NASA` is ok).
/// Plurals are also excluded (`IDs` is ok).
fn is_camel_case(s: &str) -> bool {
if s.starts_with(|c: char| c.is_ascii_digit() | c.is_ascii_lowercase()) {
return false;
}

let s = s.strip_suffix('s').unwrap_or(s);

s.chars().all(char::is_alphanumeric)
&& s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1
&& s.chars().filter(|&c| c.is_lowercase()).take(1).count() > 0
}

fn has_underscore(s: &str) -> bool {
s != "_" && !s.contains("\\_") && s.contains('_')
}

fn has_hyphen(s: &str) -> bool {
s != "-" && s.contains('-')
}

if let Ok(url) = Url::parse(word) {
// try to get around the fact that `foo::bar` parses as a valid URL
if !url.cannot_be_a_base() {
span_lint(
cx,
DOC_MARKDOWN,
span,
"you should put bare URLs between `<`/`>` or make a proper Markdown link",
);

return;
}
}

// We assume that mixed-case words are not meant to be put inside backticks. (Issue #2343)
if has_underscore(word) && has_hyphen(word) {
return;
}

if has_underscore(word) || word.contains("::") || is_camel_case(word) {
let mut applicability = Applicability::MachineApplicable;

span_lint_and_then(
cx,
DOC_MARKDOWN,
span,
"item in documentation is missing backticks",
|diag| {
let snippet = snippet_with_applicability(cx, span, "..", &mut applicability);
diag.span_suggestion_with_style(
span,
"try",
format!("`{snippet}`"),
applicability,
// always show the suggestion in a separate line, since the
// inline presentation adds another pair of backticks
SuggestionStyle::ShowAlways,
);
},
);
}
}
84 changes: 84 additions & 0 deletions clippy_lints/src/doc/missing_headers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use clippy_utils::diagnostics::{span_lint, span_lint_and_note};
use clippy_utils::ty::{implements_trait, is_type_diagnostic_item};
use clippy_utils::{is_doc_hidden, return_ty};
use rustc_hir::{BodyId, FnSig, OwnerId, Unsafety};
use rustc_lint::LateContext;
use rustc_middle::ty;
use rustc_span::{sym, Span};

use super::{DocHeaders, MISSING_ERRORS_DOC, MISSING_PANICS_DOC, MISSING_SAFETY_DOC, UNNECESSARY_SAFETY_DOC};

pub fn check(
cx: &LateContext<'_>,
owner_id: OwnerId,
sig: &FnSig<'_>,
headers: DocHeaders,
body_id: Option<BodyId>,
panic_span: Option<Span>,
) {
if !cx.effective_visibilities.is_exported(owner_id.def_id) {
return; // Private functions do not require doc comments
}

// do not lint if any parent has `#[doc(hidden)]` attribute (#7347)
if cx
.tcx
.hir()
.parent_iter(owner_id.into())
.any(|(id, _node)| is_doc_hidden(cx.tcx.hir().attrs(id)))
{
return;
}

let span = cx.tcx.def_span(owner_id);
match (headers.safety, sig.header.unsafety) {
(false, Unsafety::Unsafe) => span_lint(
cx,
MISSING_SAFETY_DOC,
span,
"unsafe function's docs miss `# Safety` section",
),
(true, Unsafety::Normal) => span_lint(
cx,
UNNECESSARY_SAFETY_DOC,
span,
"safe function's docs have unnecessary `# Safety` section",
),
_ => (),
}
if !headers.panics && panic_span.is_some() {
span_lint_and_note(
cx,
MISSING_PANICS_DOC,
span,
"docs for function which may panic missing `# Panics` section",
panic_span,
"first possible panic found here",
);
}
if !headers.errors {
if is_type_diagnostic_item(cx, return_ty(cx, owner_id), sym::Result) {
span_lint(
cx,
MISSING_ERRORS_DOC,
span,
"docs for function returning `Result` missing `# Errors` section",
);
} else if let Some(body_id) = body_id
&& let Some(future) = cx.tcx.lang_items().future_trait()
&& let typeck = cx.tcx.typeck_body(body_id)
&& let body = cx.tcx.hir().body(body_id)
&& let ret_ty = typeck.expr_ty(body.value)
&& implements_trait(cx, ret_ty, future, &[])
&& let ty::Coroutine(_, subs, _) = ret_ty.kind()
&& is_type_diagnostic_item(cx, subs.as_coroutine().return_ty(), sym::Result)
{
span_lint(
cx,
MISSING_ERRORS_DOC,
span,
"docs for function returning `Result` missing `# Errors` section",
);
}
}
}
Loading