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_entrypoint_fn, is_expn_of, match_panic_def_id, method_chain_args, return_ty}
;
4 use if_chain
::if_chain
;
5 use itertools
::Itertools
;
6 use rustc_ast
::ast
::{Async, AttrKind, Attribute, FnKind, FnRetTy, ItemKind}
;
7 use rustc_ast
::token
::CommentKind
;
8 use rustc_data_structures
::fx
::FxHashSet
;
9 use rustc_data_structures
::sync
::Lrc
;
10 use rustc_errors
::emitter
::EmitterWriter
;
11 use rustc_errors
::Handler
;
13 use rustc_hir
::intravisit
::{self, NestedVisitorMap, Visitor}
;
14 use rustc_hir
::{AnonConst, Expr, ExprKind, QPath}
;
15 use rustc_lint
::{LateContext, LateLintPass}
;
16 use rustc_middle
::hir
::map
::Map
;
17 use rustc_middle
::lint
::in_external_macro
;
19 use rustc_parse
::maybe_new_parser_from_source_str
;
20 use rustc_parse
::parser
::ForceCollect
;
21 use rustc_session
::parse
::ParseSess
;
22 use rustc_session
::{declare_tool_lint, impl_lint_pass}
;
23 use rustc_span
::edition
::Edition
;
24 use rustc_span
::source_map
::{BytePos, FilePathMapping, MultiSpan, SourceMap, Span}
;
25 use rustc_span
::{sym, FileName, Pos}
;
30 declare_clippy_lint
! {
31 /// **What it does:** Checks for the presence of `_`, `::` or camel-case words
32 /// outside ticks in documentation.
34 /// **Why is this bad?** *Rustdoc* supports markdown formatting, `_`, `::` and
35 /// camel-case probably indicates some code which should be included between
36 /// ticks. `_` can also be used for emphasis in markdown, this lint tries to
39 /// **Known problems:** Lots of bad docs won’t be fixed, what the lint checks
40 /// for is limited, and there are still false positives.
42 /// In addition, when writing documentation comments, including `[]` brackets
43 /// inside a link text would trip the parser. Therfore, documenting link with
44 /// `[`SmallVec<[T; INLINE_CAPACITY]>`]` and then [`SmallVec<[T; INLINE_CAPACITY]>`]: SmallVec
49 /// /// Do something with the foo_bar parameter. See also
50 /// /// that::other::module::foo.
51 /// // ^ `foo_bar` and `that::other::module::foo` should be ticked.
52 /// fn doit(foo_bar: usize) {}
56 /// // Link text with `[]` brackets should be written as following:
57 /// /// Consume the array and return the inner
58 /// /// [`SmallVec<[T; INLINE_CAPACITY]>`][SmallVec].
59 /// /// [SmallVec]: SmallVec
64 "presence of `_`, `::` or camel-case outside backticks in documentation"
67 declare_clippy_lint
! {
68 /// **What it does:** Checks for the doc comments of publicly visible
69 /// unsafe functions and warns if there is no `# Safety` section.
71 /// **Why is this bad?** Unsafe functions should document their safety
72 /// preconditions, so that users can be sure they are using them safely.
74 /// **Known problems:** None.
78 ///# type Universe = ();
79 /// /// This function should really be documented
80 /// pub unsafe fn start_apocalypse(u: &mut Universe) {
85 /// At least write a line about safety:
88 ///# type Universe = ();
91 /// /// This function should not be called before the horsemen are ready.
92 /// pub unsafe fn start_apocalypse(u: &mut Universe) {
96 pub MISSING_SAFETY_DOC
,
98 "`pub unsafe fn` without `# Safety` docs"
101 declare_clippy_lint
! {
102 /// **What it does:** Checks the doc comments of publicly visible functions that
103 /// return a `Result` type and warns if there is no `# Errors` section.
105 /// **Why is this bad?** Documenting the type of errors that can be returned from a
106 /// function can help callers write code to handle the errors appropriately.
108 /// **Known problems:** None.
112 /// Since the following function returns a `Result` it has an `# Errors` section in
119 /// /// Will return `Err` if `filename` does not exist or the user does not have
120 /// /// permission to read it.
121 /// pub fn read(filename: String) -> io::Result<String> {
122 /// unimplemented!();
125 pub MISSING_ERRORS_DOC
,
127 "`pub fn` returns `Result` without `# Errors` in doc comment"
130 declare_clippy_lint
! {
131 /// **What it does:** Checks the doc comments of publicly visible functions that
132 /// may panic and warns if there is no `# Panics` section.
134 /// **Why is this bad?** Documenting the scenarios in which panicking occurs
135 /// can help callers who do not want to panic to avoid those situations.
137 /// **Known problems:** None.
141 /// Since the following function may panic it has a `# Panics` section in
147 /// /// Will panic if y is 0
148 /// pub fn divide_by(x: i32, y: i32) -> i32 {
150 /// panic!("Cannot divide by 0")
156 pub MISSING_PANICS_DOC
,
158 "`pub fn` may panic without `# Panics` in doc comment"
161 declare_clippy_lint
! {
162 /// **What it does:** Checks for `fn main() { .. }` in doctests
164 /// **Why is this bad?** The test can be shorter (and likely more readable)
165 /// if the `fn main()` is left implicit.
167 /// **Known problems:** None.
171 /// /// An example of a doctest with a `main()` function
177 /// /// // this needs not be in an `fn`
180 /// fn needless_main() {
181 /// unimplemented!();
184 pub NEEDLESS_DOCTEST_MAIN
,
186 "presence of `fn main() {` in code examples"
189 #[allow(clippy::module_name_repetitions)]
191 pub struct DocMarkdown
{
192 valid_idents
: FxHashSet
<String
>,
197 pub fn new(valid_idents
: FxHashSet
<String
>) -> Self {
200 in_trait_impl
: false,
205 impl_lint_pass
!(DocMarkdown
=>
206 [DOC_MARKDOWN
, MISSING_SAFETY_DOC
, MISSING_ERRORS_DOC
, MISSING_PANICS_DOC
, NEEDLESS_DOCTEST_MAIN
]
209 impl<'tcx
> LateLintPass
<'tcx
> for DocMarkdown
{
210 fn check_crate(&mut self, cx
: &LateContext
<'tcx
>, _
: &'tcx hir
::Crate
<'_
>) {
211 let attrs
= cx
.tcx
.hir().attrs(hir
::CRATE_HIR_ID
);
212 check_attrs(cx
, &self.valid_idents
, attrs
);
215 fn check_item(&mut self, cx
: &LateContext
<'tcx
>, item
: &'tcx hir
::Item
<'_
>) {
216 let attrs
= cx
.tcx
.hir().attrs(item
.hir_id());
217 let headers
= check_attrs(cx
, &self.valid_idents
, attrs
);
219 hir
::ItemKind
::Fn(ref sig
, _
, body_id
) => {
220 if !(is_entrypoint_fn(cx
, item
.def_id
.to_def_id()) || in_external_macro(cx
.tcx
.sess
, item
.span
)) {
221 let body
= cx
.tcx
.hir().body(body_id
);
222 let mut fpu
= FindPanicUnwrap
{
224 typeck_results
: cx
.tcx
.typeck(item
.def_id
),
227 fpu
.visit_expr(&body
.value
);
228 lint_for_missing_headers(
239 hir
::ItemKind
::Impl(ref impl_
) => {
240 self.in_trait_impl
= impl_
.of_trait
.is_some();
246 fn check_item_post(&mut self, _cx
: &LateContext
<'tcx
>, item
: &'tcx hir
::Item
<'_
>) {
247 if let hir
::ItemKind
::Impl { .. }
= item
.kind
{
248 self.in_trait_impl
= false;
252 fn check_trait_item(&mut self, cx
: &LateContext
<'tcx
>, item
: &'tcx hir
::TraitItem
<'_
>) {
253 let attrs
= cx
.tcx
.hir().attrs(item
.hir_id());
254 let headers
= check_attrs(cx
, &self.valid_idents
, attrs
);
255 if let hir
::TraitItemKind
::Fn(ref sig
, ..) = item
.kind
{
256 if !in_external_macro(cx
.tcx
.sess
, item
.span
) {
257 lint_for_missing_headers(cx
, item
.hir_id(), item
.span
, sig
, headers
, None
, None
);
262 fn check_impl_item(&mut self, cx
: &LateContext
<'tcx
>, item
: &'tcx hir
::ImplItem
<'_
>) {
263 let attrs
= cx
.tcx
.hir().attrs(item
.hir_id());
264 let headers
= check_attrs(cx
, &self.valid_idents
, attrs
);
265 if self.in_trait_impl
|| in_external_macro(cx
.tcx
.sess
, item
.span
) {
268 if let hir
::ImplItemKind
::Fn(ref sig
, body_id
) = item
.kind
{
269 let body
= cx
.tcx
.hir().body(body_id
);
270 let mut fpu
= FindPanicUnwrap
{
272 typeck_results
: cx
.tcx
.typeck(item
.def_id
),
275 fpu
.visit_expr(&body
.value
);
276 lint_for_missing_headers(
289 fn lint_for_missing_headers
<'tcx
>(
290 cx
: &LateContext
<'tcx
>,
292 span
: impl Into
<MultiSpan
> + Copy
,
293 sig
: &hir
::FnSig
<'_
>,
295 body_id
: Option
<hir
::BodyId
>,
296 panic_span
: Option
<Span
>,
298 if !cx
.access_levels
.is_exported(hir_id
) {
299 return; // Private functions do not require doc comments
301 if !headers
.safety
&& sig
.header
.unsafety
== hir
::Unsafety
::Unsafe
{
306 "unsafe function's docs miss `# Safety` section",
309 if !headers
.panics
&& panic_span
.is_some() {
314 "docs for function which may panic missing `# Panics` section",
316 "first possible panic found here",
320 if is_type_diagnostic_item(cx
, return_ty(cx
, hir_id
), sym
::result_type
) {
325 "docs for function returning `Result` missing `# Errors` section",
329 if let Some(body_id
) = body_id
;
330 if let Some(future
) = cx
.tcx
.lang_items().future_trait();
331 let typeck
= cx
.tcx
.typeck_body(body_id
);
332 let body
= cx
.tcx
.hir().body(body_id
);
333 let ret_ty
= typeck
.expr_ty(&body
.value
);
334 if implements_trait(cx
, ret_ty
, future
, &[]);
335 if let ty
::Opaque(_
, subs
) = ret_ty
.kind();
336 if let Some(gen
) = subs
.types().next();
337 if let ty
::Generator(_
, subs
, _
) = gen
.kind();
338 if is_type_diagnostic_item(cx
, subs
.as_generator().return_ty(), sym
::result_type
);
344 "docs for function returning `Result` missing `# Errors` section",
352 /// Cleanup documentation decoration.
354 /// We can't use `rustc_ast::attr::AttributeMethods::with_desugared_doc` or
355 /// `rustc_ast::parse::lexer::comments::strip_doc_comment_decoration` because we
356 /// need to keep track of
357 /// the spans but this function is inspired from the later.
358 #[allow(clippy::cast_possible_truncation)]
360 pub fn strip_doc_comment_decoration(doc
: &str, comment_kind
: CommentKind
, span
: Span
) -> (String
, Vec
<(usize, Span
)>) {
361 // one-line comments lose their prefix
362 if comment_kind
== CommentKind
::Line
{
363 let mut doc
= doc
.to_owned();
366 // +3 skips the opening delimiter
367 return (doc
, vec
![(len
, span
.with_lo(span
.lo() + BytePos(3)))]);
370 let mut sizes
= vec
![];
371 let mut contains_initial_stars
= false;
372 for line
in doc
.lines() {
373 let offset
= line
.as_ptr() as usize - doc
.as_ptr() as usize;
374 debug_assert_eq
!(offset
as u32 as usize, offset
);
375 contains_initial_stars
|= line
.trim_start().starts_with('
*'
);
376 // +1 adds the newline, +3 skips the opening delimiter
377 sizes
.push((line
.len() + 1, span
.with_lo(span
.lo() + BytePos(3 + offset
as u32))));
379 if !contains_initial_stars
{
380 return (doc
.to_string(), sizes
);
382 // remove the initial '*'s if any
383 let mut no_stars
= String
::with_capacity(doc
.len());
384 for line
in doc
.lines() {
385 let mut chars
= line
.chars();
386 for c
in &mut chars
{
387 if c
.is_whitespace() {
390 no_stars
.push(if c
== '
*' { ' ' }
else { c }
);
394 no_stars
.push_str(chars
.as_str());
401 #[derive(Copy, Clone)]
408 fn check_attrs
<'a
>(cx
: &LateContext
<'_
>, valid_idents
: &FxHashSet
<String
>, attrs
: &'a
[Attribute
]) -> DocHeaders
{
409 let mut doc
= String
::new();
410 let mut spans
= vec
![];
413 if let AttrKind
::DocComment(comment_kind
, comment
) = attr
.kind
{
414 let (comment
, current_spans
) = strip_doc_comment_decoration(&comment
.as_str(), comment_kind
, attr
.span
);
415 spans
.extend_from_slice(¤t_spans
);
416 doc
.push_str(&comment
);
417 } else if attr
.has_name(sym
::doc
) {
418 // ignore mix of sugared and non-sugared doc
419 // don't trigger the safety or errors check
429 for &mut (ref mut offset
, _
) in &mut spans
{
430 let offset_copy
= *offset
;
432 current
+= offset_copy
;
443 let parser
= pulldown_cmark
::Parser
::new(&doc
).into_offset_iter();
444 // Iterate over all `Events` and combine consecutive events into one
445 let events
= parser
.coalesce(|previous
, current
| {
446 use pulldown_cmark
::Event
::Text
;
448 let previous_range
= previous
.1;
449 let current_range
= current
.1;
451 match (previous
.0, current
.0) {
452 (Text(previous
), Text(current
)) => {
453 let mut previous
= previous
.to_string();
454 previous
.push_str(¤t
);
455 Ok((Text(previous
.into()), previous_range
))
457 (previous
, current
) => Err(((previous
, previous_range
), (current
, current_range
))),
460 check_doc(cx
, valid_idents
, events
, &spans
)
463 const RUST_CODE
: &[&str] = &["rust", "no_run", "should_panic", "compile_fail"];
465 fn check_doc
<'a
, Events
: Iterator
<Item
= (pulldown_cmark
::Event
<'a
>, Range
<usize>)>>(
466 cx
: &LateContext
<'_
>,
467 valid_idents
: &FxHashSet
<String
>,
469 spans
: &[(usize, Span
)],
471 // true if a safety header was found
472 use pulldown_cmark
::CodeBlockKind
;
473 use pulldown_cmark
::Event
::{
474 Code
, End
, FootnoteReference
, HardBreak
, Html
, Rule
, SoftBreak
, Start
, TaskListMarker
, Text
,
476 use pulldown_cmark
::Tag
::{CodeBlock, Heading, Link}
;
478 let mut headers
= DocHeaders
{
483 let mut in_code
= false;
484 let mut in_link
= None
;
485 let mut in_heading
= false;
486 let mut is_rust
= false;
487 let mut edition
= None
;
488 for (event
, range
) in events
{
490 Start(CodeBlock(ref kind
)) => {
492 if let CodeBlockKind
::Fenced(lang
) = kind
{
493 for item
in lang
.split('
,'
) {
494 if item
== "ignore" {
498 if let Some(stripped
) = item
.strip_prefix("edition") {
500 edition
= stripped
.parse
::<Edition
>().ok();
501 } else if item
.is_empty() || RUST_CODE
.contains(&item
) {
507 End(CodeBlock(_
)) => {
511 Start(Link(_
, url
, _
)) => in_link
= Some(url
),
512 End(Link(..)) => in_link
= None
,
513 Start(Heading(_
)) => in_heading
= true,
514 End(Heading(_
)) => in_heading
= false,
515 Start(_tag
) | End(_tag
) => (), // We don't care about other tags
516 Html(_html
) => (), // HTML is weird, just ignore it
517 SoftBreak
| HardBreak
| TaskListMarker(_
) | Code(_
) | Rule
=> (),
518 FootnoteReference(text
) | Text(text
) => {
519 if Some(&text
) == in_link
.as_ref() {
520 // Probably a link of the form `<http://example.com>`
521 // Which are represented as a link to "http://example.com" with
522 // text "http://example.com" by pulldown-cmark
525 headers
.safety
|= in_heading
&& text
.trim() == "Safety";
526 headers
.errors
|= in_heading
&& text
.trim() == "Errors";
527 headers
.panics
|= in_heading
&& text
.trim() == "Panics";
528 let index
= match spans
.binary_search_by(|c
| c
.0.cmp(&range
.start
)) {
532 let (begin
, span
) = spans
[index
];
535 let edition
= edition
.unwrap_or_else(|| cx
.tcx
.sess
.edition());
536 check_code(cx
, &text
, edition
, span
);
539 // Adjust for the beginning of the current `Event`
540 let span
= span
.with_lo(span
.lo() + BytePos
::from_usize(range
.start
- begin
));
542 check_text(cx
, valid_idents
, &text
, span
);
550 fn check_code(cx
: &LateContext
<'_
>, text
: &str, edition
: Edition
, span
: Span
) {
551 fn has_needless_main(code
: &str, edition
: Edition
) -> bool
{
552 rustc_driver
::catch_fatal_errors(|| {
553 rustc_span
::with_session_globals(edition
, || {
554 let filename
= FileName
::anon_source_code(code
);
556 let sm
= Lrc
::new(SourceMap
::new(FilePathMapping
::empty()));
557 let emitter
= EmitterWriter
::new(box io
::sink(), None
, false, false, false, None
, false);
558 let handler
= Handler
::with_emitter(false, None
, box emitter
);
559 let sess
= ParseSess
::with_span_handler(handler
, sm
);
561 let mut parser
= match maybe_new_parser_from_source_str(&sess
, filename
, code
.into()) {
564 for mut err
in errs
{
571 let mut relevant_main_found
= false;
573 match parser
.parse_item(ForceCollect
::No
) {
574 Ok(Some(item
)) => match &item
.kind
{
575 // Tests with one of these items are ignored
577 | ItemKind
::Const(..)
578 | ItemKind
::ExternCrate(..)
579 | ItemKind
::ForeignMod(..) => return false,
580 // We found a main function ...
581 ItemKind
::Fn(box FnKind(_
, sig
, _
, Some(block
))) if item
.ident
.name
== sym
::main
=> {
582 let is_async
= matches
!(sig
.header
.asyncness
, Async
::Yes { .. }
);
583 let returns_nothing
= match &sig
.decl
.output
{
584 FnRetTy
::Default(..) => true,
585 FnRetTy
::Ty(ty
) if ty
.kind
.is_unit() => true,
586 FnRetTy
::Ty(_
) => false,
589 if returns_nothing
&& !is_async
&& !block
.stmts
.is_empty() {
590 // This main function should be linted, but only if there are no other functions
591 relevant_main_found
= true;
593 // This main function should not be linted, we're done
597 // Another function was found; this case is ignored too
598 ItemKind
::Fn(..) => return false,
616 if has_needless_main(text
, edition
) {
617 span_lint(cx
, NEEDLESS_DOCTEST_MAIN
, span
, "needless `fn main` in doctest");
621 fn check_text(cx
: &LateContext
<'_
>, valid_idents
: &FxHashSet
<String
>, text
: &str, span
: Span
) {
622 for word
in text
.split(|c
: char| c
.is_whitespace() || c
== '
\''
) {
623 // Trim punctuation as in `some comment (see foo::bar).`
625 // Or even as in `_foo bar_` which is emphasized.
626 let word
= word
.trim_matches(|c
: char| !c
.is_alphanumeric());
628 if valid_idents
.contains(word
) {
632 // Adjust for the current word
633 let offset
= word
.as_ptr() as usize - text
.as_ptr() as usize;
634 let span
= Span
::new(
635 span
.lo() + BytePos
::from_usize(offset
),
636 span
.lo() + BytePos
::from_usize(offset
+ word
.len()),
640 check_word(cx
, word
, span
);
644 fn check_word(cx
: &LateContext
<'_
>, word
: &str, span
: Span
) {
645 /// Checks if a string is camel-case, i.e., contains at least two uppercase
646 /// letters (`Clippy` is ok) and one lower-case letter (`NASA` is ok).
647 /// Plurals are also excluded (`IDs` is ok).
648 fn is_camel_case(s
: &str) -> bool
{
649 if s
.starts_with(|c
: char| c
.is_digit(10)) {
653 let s
= s
.strip_suffix('s'
).unwrap_or(s
);
655 s
.chars().all(char::is_alphanumeric
)
656 && s
.chars().filter(|&c
| c
.is_uppercase()).take(2).count() > 1
657 && s
.chars().filter(|&c
| c
.is_lowercase()).take(1).count() > 0
660 fn has_underscore(s
: &str) -> bool
{
661 s
!= "_" && !s
.contains("\\_") && s
.contains('_'
)
664 fn has_hyphen(s
: &str) -> bool
{
665 s
!= "-" && s
.contains('
-'
)
668 if let Ok(url
) = Url
::parse(word
) {
669 // try to get around the fact that `foo::bar` parses as a valid URL
670 if !url
.cannot_be_a_base() {
675 "you should put bare URLs between `<`/`>` or make a proper Markdown link",
682 // We assume that mixed-case words are not meant to be put inside bacticks. (Issue #2343)
683 if has_underscore(word
) && has_hyphen(word
) {
687 if has_underscore(word
) || word
.contains("::") || is_camel_case(word
) {
692 &format
!("you should put `{}` between ticks in the documentation", word
),
697 struct FindPanicUnwrap
<'a
, 'tcx
> {
698 cx
: &'a LateContext
<'tcx
>,
699 panic_span
: Option
<Span
>,
700 typeck_results
: &'tcx ty
::TypeckResults
<'tcx
>,
703 impl<'a
, 'tcx
> Visitor
<'tcx
> for FindPanicUnwrap
<'a
, 'tcx
> {
704 type Map
= Map
<'tcx
>;
706 fn visit_expr(&mut self, expr
: &'tcx Expr
<'_
>) {
707 if self.panic_span
.is_some() {
711 // check for `begin_panic`
713 if let ExprKind
::Call(func_expr
, _
) = expr
.kind
;
714 if let ExprKind
::Path(QPath
::Resolved(_
, path
)) = func_expr
.kind
;
715 if let Some(path_def_id
) = path
.res
.opt_def_id();
716 if match_panic_def_id(self.cx
, path_def_id
);
717 if is_expn_of(expr
.span
, "unreachable").is_none();
718 if !is_expn_of_debug_assertions(expr
.span
);
720 self.panic_span
= Some(expr
.span
);
724 // check for `assert_eq` or `assert_ne`
725 if is_expn_of(expr
.span
, "assert_eq").is_some() || is_expn_of(expr
.span
, "assert_ne").is_some() {
726 self.panic_span
= Some(expr
.span
);
729 // check for `unwrap`
730 if let Some(arglists
) = method_chain_args(expr
, &["unwrap"]) {
731 let reciever_ty
= self.typeck_results
.expr_ty(&arglists
[0][0]).peel_refs();
732 if is_type_diagnostic_item(self.cx
, reciever_ty
, sym
::option_type
)
733 || is_type_diagnostic_item(self.cx
, reciever_ty
, sym
::result_type
)
735 self.panic_span
= Some(expr
.span
);
739 // and check sub-expressions
740 intravisit
::walk_expr(self, expr
);
743 // Panics in const blocks will cause compilation to fail.
744 fn visit_anon_const(&mut self, _
: &'tcx AnonConst
) {}
746 fn nested_visit_map(&mut self) -> NestedVisitorMap
<Self::Map
> {
747 NestedVisitorMap
::OnlyBodies(self.cx
.tcx
.hir())
751 fn is_expn_of_debug_assertions(span
: Span
) -> bool
{
752 const MACRO_NAMES
: &[&str] = &["debug_assert", "debug_assert_eq", "debug_assert_ne"];
753 MACRO_NAMES
.iter().any(|name
| is_expn_of(span
, name
).is_some())