Skip to content

Commit 924888d

Browse files
committed
split up doc.rs
1 parent a4b2864 commit 924888d

File tree

7 files changed

+425
-352
lines changed

7 files changed

+425
-352
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use std::ops::Range;
2+
3+
use clippy_utils::diagnostics::span_lint;
4+
use rustc_lint::LateContext;
5+
6+
use super::{Fragments, DOC_LINK_WITH_QUOTES};
7+
8+
pub fn check(cx: &LateContext<'_>, trimmed_text: &str, range: Range<usize>, fragments: Fragments<'_>) {
9+
if trimmed_text.starts_with('\'')
10+
&& trimmed_text.ends_with('\'')
11+
&& let Some(span) = fragments.span(cx, range)
12+
{
13+
span_lint(
14+
cx,
15+
DOC_LINK_WITH_QUOTES,
16+
span,
17+
"possible intra-doc link using quotes instead of backticks",
18+
);
19+
}
20+
}

clippy_lints/src/doc/markdown.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
2+
use clippy_utils::source::snippet_with_applicability;
3+
use rustc_data_structures::fx::FxHashSet;
4+
use rustc_errors::{Applicability, SuggestionStyle};
5+
use rustc_lint::LateContext;
6+
use rustc_span::{BytePos, Pos, Span};
7+
use url::Url;
8+
9+
use crate::doc::DOC_MARKDOWN;
10+
11+
pub fn check(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, text: &str, span: Span) {
12+
for word in text.split(|c: char| c.is_whitespace() || c == '\'') {
13+
// Trim punctuation as in `some comment (see foo::bar).`
14+
// ^^
15+
// Or even as in `_foo bar_` which is emphasized. Also preserve `::` as a prefix/suffix.
16+
let mut word = word.trim_matches(|c: char| !c.is_alphanumeric() && c != ':');
17+
18+
// Remove leading or trailing single `:` which may be part of a sentence.
19+
if word.starts_with(':') && !word.starts_with("::") {
20+
word = word.trim_start_matches(':');
21+
}
22+
if word.ends_with(':') && !word.ends_with("::") {
23+
word = word.trim_end_matches(':');
24+
}
25+
26+
if valid_idents.contains(word) || word.chars().all(|c| c == ':') {
27+
continue;
28+
}
29+
30+
// Adjust for the current word
31+
let offset = word.as_ptr() as usize - text.as_ptr() as usize;
32+
let span = Span::new(
33+
span.lo() + BytePos::from_usize(offset),
34+
span.lo() + BytePos::from_usize(offset + word.len()),
35+
span.ctxt(),
36+
span.parent(),
37+
);
38+
39+
check_word(cx, word, span);
40+
}
41+
}
42+
43+
fn check_word(cx: &LateContext<'_>, word: &str, span: Span) {
44+
/// Checks if a string is upper-camel-case, i.e., starts with an uppercase and
45+
/// contains at least two uppercase letters (`Clippy` is ok) and one lower-case
46+
/// letter (`NASA` is ok).
47+
/// Plurals are also excluded (`IDs` is ok).
48+
fn is_camel_case(s: &str) -> bool {
49+
if s.starts_with(|c: char| c.is_ascii_digit() | c.is_ascii_lowercase()) {
50+
return false;
51+
}
52+
53+
let s = s.strip_suffix('s').unwrap_or(s);
54+
55+
s.chars().all(char::is_alphanumeric)
56+
&& s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1
57+
&& s.chars().filter(|&c| c.is_lowercase()).take(1).count() > 0
58+
}
59+
60+
fn has_underscore(s: &str) -> bool {
61+
s != "_" && !s.contains("\\_") && s.contains('_')
62+
}
63+
64+
fn has_hyphen(s: &str) -> bool {
65+
s != "-" && s.contains('-')
66+
}
67+
68+
if let Ok(url) = Url::parse(word) {
69+
// try to get around the fact that `foo::bar` parses as a valid URL
70+
if !url.cannot_be_a_base() {
71+
span_lint(
72+
cx,
73+
DOC_MARKDOWN,
74+
span,
75+
"you should put bare URLs between `<`/`>` or make a proper Markdown link",
76+
);
77+
78+
return;
79+
}
80+
}
81+
82+
// We assume that mixed-case words are not meant to be put inside backticks. (Issue #2343)
83+
if has_underscore(word) && has_hyphen(word) {
84+
return;
85+
}
86+
87+
if has_underscore(word) || word.contains("::") || is_camel_case(word) {
88+
let mut applicability = Applicability::MachineApplicable;
89+
90+
span_lint_and_then(
91+
cx,
92+
DOC_MARKDOWN,
93+
span,
94+
"item in documentation is missing backticks",
95+
|diag| {
96+
let snippet = snippet_with_applicability(cx, span, "..", &mut applicability);
97+
diag.span_suggestion_with_style(
98+
span,
99+
"try",
100+
format!("`{snippet}`"),
101+
applicability,
102+
// always show the suggestion in a separate line, since the
103+
// inline presentation adds another pair of backticks
104+
SuggestionStyle::ShowAlways,
105+
);
106+
},
107+
);
108+
}
109+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use clippy_utils::diagnostics::{span_lint, span_lint_and_note};
2+
use clippy_utils::ty::{implements_trait, is_type_diagnostic_item};
3+
use clippy_utils::{is_doc_hidden, return_ty};
4+
use rustc_hir::{BodyId, FnSig, OwnerId, Unsafety};
5+
use rustc_lint::LateContext;
6+
use rustc_middle::ty;
7+
use rustc_span::{sym, Span};
8+
9+
use super::{DocHeaders, MISSING_ERRORS_DOC, MISSING_PANICS_DOC, MISSING_SAFETY_DOC, UNNECESSARY_SAFETY_DOC};
10+
11+
pub fn check(
12+
cx: &LateContext<'_>,
13+
owner_id: OwnerId,
14+
sig: &FnSig<'_>,
15+
headers: DocHeaders,
16+
body_id: Option<BodyId>,
17+
panic_span: Option<Span>,
18+
) {
19+
if !cx.effective_visibilities.is_exported(owner_id.def_id) {
20+
return; // Private functions do not require doc comments
21+
}
22+
23+
// do not lint if any parent has `#[doc(hidden)]` attribute (#7347)
24+
if cx
25+
.tcx
26+
.hir()
27+
.parent_iter(owner_id.into())
28+
.any(|(id, _node)| is_doc_hidden(cx.tcx.hir().attrs(id)))
29+
{
30+
return;
31+
}
32+
33+
let span = cx.tcx.def_span(owner_id);
34+
match (headers.safety, sig.header.unsafety) {
35+
(false, Unsafety::Unsafe) => span_lint(
36+
cx,
37+
MISSING_SAFETY_DOC,
38+
span,
39+
"unsafe function's docs miss `# Safety` section",
40+
),
41+
(true, Unsafety::Normal) => span_lint(
42+
cx,
43+
UNNECESSARY_SAFETY_DOC,
44+
span,
45+
"safe function's docs have unnecessary `# Safety` section",
46+
),
47+
_ => (),
48+
}
49+
if !headers.panics && panic_span.is_some() {
50+
span_lint_and_note(
51+
cx,
52+
MISSING_PANICS_DOC,
53+
span,
54+
"docs for function which may panic missing `# Panics` section",
55+
panic_span,
56+
"first possible panic found here",
57+
);
58+
}
59+
if !headers.errors {
60+
if is_type_diagnostic_item(cx, return_ty(cx, owner_id), sym::Result) {
61+
span_lint(
62+
cx,
63+
MISSING_ERRORS_DOC,
64+
span,
65+
"docs for function returning `Result` missing `# Errors` section",
66+
);
67+
} else if let Some(body_id) = body_id
68+
&& let Some(future) = cx.tcx.lang_items().future_trait()
69+
&& let typeck = cx.tcx.typeck_body(body_id)
70+
&& let body = cx.tcx.hir().body(body_id)
71+
&& let ret_ty = typeck.expr_ty(body.value)
72+
&& implements_trait(cx, ret_ty, future, &[])
73+
&& let ty::Coroutine(_, subs, _) = ret_ty.kind()
74+
&& is_type_diagnostic_item(cx, subs.as_coroutine().return_ty(), sym::Result)
75+
{
76+
span_lint(
77+
cx,
78+
MISSING_ERRORS_DOC,
79+
span,
80+
"docs for function returning `Result` missing `# Errors` section",
81+
);
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)