]>
Commit | Line | Data |
---|---|---|
3c0e092e | 1 | use clippy_utils::attrs::is_doc_hidden; |
a2a8927a | 2 | use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_note, span_lint_and_then}; |
5099ac24 | 3 | use clippy_utils::macros::{is_panic, root_macro_call_first_node}; |
3c0e092e | 4 | use clippy_utils::source::{first_line_of_span, snippet_with_applicability}; |
cdc7bbd5 | 5 | use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; |
5099ac24 | 6 | use clippy_utils::{is_entrypoint_fn, method_chain_args, return_ty}; |
f20569fa XL |
7 | use if_chain::if_chain; |
8 | use itertools::Itertools; | |
3c0e092e | 9 | use rustc_ast::ast::{Async, AttrKind, Attribute, Fn, FnRetTy, ItemKind}; |
f20569fa XL |
10 | use rustc_ast::token::CommentKind; |
11 | use rustc_data_structures::fx::FxHashSet; | |
12 | use rustc_data_structures::sync::Lrc; | |
13 | use rustc_errors::emitter::EmitterWriter; | |
04454e1e | 14 | use rustc_errors::{Applicability, Handler, MultiSpan, SuggestionStyle}; |
f20569fa | 15 | use rustc_hir as hir; |
5099ac24 FG |
16 | use rustc_hir::intravisit::{self, Visitor}; |
17 | use rustc_hir::{AnonConst, Expr}; | |
f20569fa | 18 | use rustc_lint::{LateContext, LateLintPass}; |
5099ac24 | 19 | use rustc_middle::hir::nested_filter; |
f20569fa XL |
20 | use rustc_middle::lint::in_external_macro; |
21 | use rustc_middle::ty; | |
22 | use rustc_parse::maybe_new_parser_from_source_str; | |
23 | use rustc_parse::parser::ForceCollect; | |
24 | use rustc_session::parse::ParseSess; | |
25 | use rustc_session::{declare_tool_lint, impl_lint_pass}; | |
94222f64 | 26 | use rustc_span::def_id::LocalDefId; |
f20569fa | 27 | use rustc_span::edition::Edition; |
04454e1e | 28 | use rustc_span::source_map::{BytePos, FilePathMapping, SourceMap, Span}; |
f20569fa XL |
29 | use rustc_span::{sym, FileName, Pos}; |
30 | use std::io; | |
31 | use std::ops::Range; | |
136023e0 | 32 | use std::thread; |
f20569fa XL |
33 | use url::Url; |
34 | ||
35 | declare_clippy_lint! { | |
94222f64 XL |
36 | /// ### What it does |
37 | /// Checks for the presence of `_`, `::` or camel-case words | |
f20569fa XL |
38 | /// outside ticks in documentation. |
39 | /// | |
94222f64 XL |
40 | /// ### Why is this bad? |
41 | /// *Rustdoc* supports markdown formatting, `_`, `::` and | |
f20569fa XL |
42 | /// camel-case probably indicates some code which should be included between |
43 | /// ticks. `_` can also be used for emphasis in markdown, this lint tries to | |
44 | /// consider that. | |
45 | /// | |
94222f64 XL |
46 | /// ### Known problems |
47 | /// Lots of bad docs won’t be fixed, what the lint checks | |
136023e0 XL |
48 | /// for is limited, and there are still false positives. HTML elements and their |
49 | /// content are not linted. | |
f20569fa XL |
50 | /// |
51 | /// In addition, when writing documentation comments, including `[]` brackets | |
3c0e092e | 52 | /// inside a link text would trip the parser. Therefore, documenting link with |
f20569fa XL |
53 | /// `[`SmallVec<[T; INLINE_CAPACITY]>`]` and then [`SmallVec<[T; INLINE_CAPACITY]>`]: SmallVec |
54 | /// would fail. | |
55 | /// | |
94222f64 | 56 | /// ### Examples |
f20569fa XL |
57 | /// ```rust |
58 | /// /// Do something with the foo_bar parameter. See also | |
59 | /// /// that::other::module::foo. | |
60 | /// // ^ `foo_bar` and `that::other::module::foo` should be ticked. | |
61 | /// fn doit(foo_bar: usize) {} | |
62 | /// ``` | |
63 | /// | |
64 | /// ```rust | |
65 | /// // Link text with `[]` brackets should be written as following: | |
66 | /// /// Consume the array and return the inner | |
67 | /// /// [`SmallVec<[T; INLINE_CAPACITY]>`][SmallVec]. | |
68 | /// /// [SmallVec]: SmallVec | |
69 | /// fn main() {} | |
70 | /// ``` | |
a2a8927a | 71 | #[clippy::version = "pre 1.29.0"] |
f20569fa XL |
72 | pub DOC_MARKDOWN, |
73 | pedantic, | |
74 | "presence of `_`, `::` or camel-case outside backticks in documentation" | |
75 | } | |
76 | ||
77 | declare_clippy_lint! { | |
94222f64 XL |
78 | /// ### What it does |
79 | /// Checks for the doc comments of publicly visible | |
f20569fa XL |
80 | /// unsafe functions and warns if there is no `# Safety` section. |
81 | /// | |
94222f64 XL |
82 | /// ### Why is this bad? |
83 | /// Unsafe functions should document their safety | |
f20569fa XL |
84 | /// preconditions, so that users can be sure they are using them safely. |
85 | /// | |
94222f64 | 86 | /// ### Examples |
f20569fa XL |
87 | /// ```rust |
88 | ///# type Universe = (); | |
89 | /// /// This function should really be documented | |
90 | /// pub unsafe fn start_apocalypse(u: &mut Universe) { | |
91 | /// unimplemented!(); | |
92 | /// } | |
93 | /// ``` | |
94 | /// | |
95 | /// At least write a line about safety: | |
96 | /// | |
97 | /// ```rust | |
98 | ///# type Universe = (); | |
99 | /// /// # Safety | |
100 | /// /// | |
101 | /// /// This function should not be called before the horsemen are ready. | |
102 | /// pub unsafe fn start_apocalypse(u: &mut Universe) { | |
103 | /// unimplemented!(); | |
104 | /// } | |
105 | /// ``` | |
a2a8927a | 106 | #[clippy::version = "1.39.0"] |
f20569fa XL |
107 | pub MISSING_SAFETY_DOC, |
108 | style, | |
109 | "`pub unsafe fn` without `# Safety` docs" | |
110 | } | |
111 | ||
112 | declare_clippy_lint! { | |
94222f64 XL |
113 | /// ### What it does |
114 | /// Checks the doc comments of publicly visible functions that | |
f20569fa XL |
115 | /// return a `Result` type and warns if there is no `# Errors` section. |
116 | /// | |
94222f64 XL |
117 | /// ### Why is this bad? |
118 | /// Documenting the type of errors that can be returned from a | |
f20569fa XL |
119 | /// function can help callers write code to handle the errors appropriately. |
120 | /// | |
94222f64 | 121 | /// ### Examples |
f20569fa XL |
122 | /// Since the following function returns a `Result` it has an `# Errors` section in |
123 | /// its doc comment: | |
124 | /// | |
125 | /// ```rust | |
126 | ///# use std::io; | |
127 | /// /// # Errors | |
128 | /// /// | |
129 | /// /// Will return `Err` if `filename` does not exist or the user does not have | |
130 | /// /// permission to read it. | |
131 | /// pub fn read(filename: String) -> io::Result<String> { | |
132 | /// unimplemented!(); | |
133 | /// } | |
134 | /// ``` | |
a2a8927a | 135 | #[clippy::version = "1.41.0"] |
f20569fa XL |
136 | pub MISSING_ERRORS_DOC, |
137 | pedantic, | |
138 | "`pub fn` returns `Result` without `# Errors` in doc comment" | |
139 | } | |
140 | ||
141 | declare_clippy_lint! { | |
94222f64 XL |
142 | /// ### What it does |
143 | /// Checks the doc comments of publicly visible functions that | |
f20569fa XL |
144 | /// may panic and warns if there is no `# Panics` section. |
145 | /// | |
94222f64 XL |
146 | /// ### Why is this bad? |
147 | /// Documenting the scenarios in which panicking occurs | |
f20569fa XL |
148 | /// can help callers who do not want to panic to avoid those situations. |
149 | /// | |
94222f64 | 150 | /// ### Examples |
f20569fa XL |
151 | /// Since the following function may panic it has a `# Panics` section in |
152 | /// its doc comment: | |
153 | /// | |
154 | /// ```rust | |
155 | /// /// # Panics | |
156 | /// /// | |
157 | /// /// Will panic if y is 0 | |
158 | /// pub fn divide_by(x: i32, y: i32) -> i32 { | |
159 | /// if y == 0 { | |
160 | /// panic!("Cannot divide by 0") | |
161 | /// } else { | |
162 | /// x / y | |
163 | /// } | |
164 | /// } | |
165 | /// ``` | |
923072b8 | 166 | #[clippy::version = "1.51.0"] |
f20569fa XL |
167 | pub MISSING_PANICS_DOC, |
168 | pedantic, | |
169 | "`pub fn` may panic without `# Panics` in doc comment" | |
170 | } | |
171 | ||
172 | declare_clippy_lint! { | |
94222f64 XL |
173 | /// ### What it does |
174 | /// Checks for `fn main() { .. }` in doctests | |
f20569fa | 175 | /// |
94222f64 XL |
176 | /// ### Why is this bad? |
177 | /// The test can be shorter (and likely more readable) | |
f20569fa XL |
178 | /// if the `fn main()` is left implicit. |
179 | /// | |
94222f64 | 180 | /// ### Examples |
923072b8 | 181 | /// ```rust |
f20569fa XL |
182 | /// /// An example of a doctest with a `main()` function |
183 | /// /// | |
184 | /// /// # Examples | |
185 | /// /// | |
186 | /// /// ``` | |
187 | /// /// fn main() { | |
188 | /// /// // this needs not be in an `fn` | |
189 | /// /// } | |
190 | /// /// ``` | |
191 | /// fn needless_main() { | |
192 | /// unimplemented!(); | |
193 | /// } | |
923072b8 | 194 | /// ``` |
a2a8927a | 195 | #[clippy::version = "1.40.0"] |
f20569fa XL |
196 | pub NEEDLESS_DOCTEST_MAIN, |
197 | style, | |
198 | "presence of `fn main() {` in code examples" | |
199 | } | |
200 | ||
923072b8 | 201 | #[expect(clippy::module_name_repetitions)] |
f20569fa XL |
202 | #[derive(Clone)] |
203 | pub struct DocMarkdown { | |
204 | valid_idents: FxHashSet<String>, | |
205 | in_trait_impl: bool, | |
206 | } | |
207 | ||
208 | impl DocMarkdown { | |
209 | pub fn new(valid_idents: FxHashSet<String>) -> Self { | |
210 | Self { | |
211 | valid_idents, | |
212 | in_trait_impl: false, | |
213 | } | |
214 | } | |
215 | } | |
216 | ||
217 | impl_lint_pass!(DocMarkdown => | |
218 | [DOC_MARKDOWN, MISSING_SAFETY_DOC, MISSING_ERRORS_DOC, MISSING_PANICS_DOC, NEEDLESS_DOCTEST_MAIN] | |
219 | ); | |
220 | ||
221 | impl<'tcx> LateLintPass<'tcx> for DocMarkdown { | |
c295e0f8 | 222 | fn check_crate(&mut self, cx: &LateContext<'tcx>) { |
f20569fa XL |
223 | let attrs = cx.tcx.hir().attrs(hir::CRATE_HIR_ID); |
224 | check_attrs(cx, &self.valid_idents, attrs); | |
225 | } | |
226 | ||
227 | fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { | |
228 | let attrs = cx.tcx.hir().attrs(item.hir_id()); | |
229 | let headers = check_attrs(cx, &self.valid_idents, attrs); | |
230 | match item.kind { | |
231 | hir::ItemKind::Fn(ref sig, _, body_id) => { | |
232 | if !(is_entrypoint_fn(cx, item.def_id.to_def_id()) || in_external_macro(cx.tcx.sess, item.span)) { | |
233 | let body = cx.tcx.hir().body(body_id); | |
234 | let mut fpu = FindPanicUnwrap { | |
235 | cx, | |
236 | typeck_results: cx.tcx.typeck(item.def_id), | |
237 | panic_span: None, | |
238 | }; | |
239 | fpu.visit_expr(&body.value); | |
94222f64 | 240 | lint_for_missing_headers(cx, item.def_id, item.span, sig, headers, Some(body_id), fpu.panic_span); |
f20569fa XL |
241 | } |
242 | }, | |
04454e1e | 243 | hir::ItemKind::Impl(impl_) => { |
f20569fa XL |
244 | self.in_trait_impl = impl_.of_trait.is_some(); |
245 | }, | |
c295e0f8 XL |
246 | hir::ItemKind::Trait(_, unsafety, ..) => { |
247 | if !headers.safety && unsafety == hir::Unsafety::Unsafe { | |
248 | span_lint( | |
249 | cx, | |
250 | MISSING_SAFETY_DOC, | |
251 | item.span, | |
252 | "docs for unsafe trait missing `# Safety` section", | |
253 | ); | |
254 | } | |
255 | }, | |
256 | _ => (), | |
f20569fa XL |
257 | } |
258 | } | |
259 | ||
260 | fn check_item_post(&mut self, _cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { | |
261 | if let hir::ItemKind::Impl { .. } = item.kind { | |
262 | self.in_trait_impl = false; | |
263 | } | |
264 | } | |
265 | ||
266 | fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'_>) { | |
267 | let attrs = cx.tcx.hir().attrs(item.hir_id()); | |
268 | let headers = check_attrs(cx, &self.valid_idents, attrs); | |
269 | if let hir::TraitItemKind::Fn(ref sig, ..) = item.kind { | |
270 | if !in_external_macro(cx.tcx.sess, item.span) { | |
94222f64 | 271 | lint_for_missing_headers(cx, item.def_id, item.span, sig, headers, None, None); |
f20569fa XL |
272 | } |
273 | } | |
274 | } | |
275 | ||
276 | fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::ImplItem<'_>) { | |
277 | let attrs = cx.tcx.hir().attrs(item.hir_id()); | |
278 | let headers = check_attrs(cx, &self.valid_idents, attrs); | |
279 | if self.in_trait_impl || in_external_macro(cx.tcx.sess, item.span) { | |
280 | return; | |
281 | } | |
282 | if let hir::ImplItemKind::Fn(ref sig, body_id) = item.kind { | |
283 | let body = cx.tcx.hir().body(body_id); | |
284 | let mut fpu = FindPanicUnwrap { | |
285 | cx, | |
286 | typeck_results: cx.tcx.typeck(item.def_id), | |
287 | panic_span: None, | |
288 | }; | |
289 | fpu.visit_expr(&body.value); | |
94222f64 | 290 | lint_for_missing_headers(cx, item.def_id, item.span, sig, headers, Some(body_id), fpu.panic_span); |
f20569fa XL |
291 | } |
292 | } | |
293 | } | |
294 | ||
295 | fn lint_for_missing_headers<'tcx>( | |
296 | cx: &LateContext<'tcx>, | |
94222f64 | 297 | def_id: LocalDefId, |
f20569fa XL |
298 | span: impl Into<MultiSpan> + Copy, |
299 | sig: &hir::FnSig<'_>, | |
300 | headers: DocHeaders, | |
301 | body_id: Option<hir::BodyId>, | |
302 | panic_span: Option<Span>, | |
303 | ) { | |
94222f64 | 304 | if !cx.access_levels.is_exported(def_id) { |
f20569fa XL |
305 | return; // Private functions do not require doc comments |
306 | } | |
3c0e092e XL |
307 | |
308 | // do not lint if any parent has `#[doc(hidden)]` attribute (#7347) | |
309 | if cx | |
310 | .tcx | |
311 | .hir() | |
312 | .parent_iter(cx.tcx.hir().local_def_id_to_hir_id(def_id)) | |
313 | .any(|(id, _node)| is_doc_hidden(cx.tcx.hir().attrs(id))) | |
314 | { | |
315 | return; | |
316 | } | |
317 | ||
f20569fa XL |
318 | if !headers.safety && sig.header.unsafety == hir::Unsafety::Unsafe { |
319 | span_lint( | |
320 | cx, | |
321 | MISSING_SAFETY_DOC, | |
322 | span, | |
323 | "unsafe function's docs miss `# Safety` section", | |
324 | ); | |
325 | } | |
326 | if !headers.panics && panic_span.is_some() { | |
327 | span_lint_and_note( | |
328 | cx, | |
329 | MISSING_PANICS_DOC, | |
330 | span, | |
331 | "docs for function which may panic missing `# Panics` section", | |
332 | panic_span, | |
333 | "first possible panic found here", | |
334 | ); | |
335 | } | |
336 | if !headers.errors { | |
94222f64 | 337 | let hir_id = cx.tcx.hir().local_def_id_to_hir_id(def_id); |
c295e0f8 | 338 | if is_type_diagnostic_item(cx, return_ty(cx, hir_id), sym::Result) { |
f20569fa XL |
339 | span_lint( |
340 | cx, | |
341 | MISSING_ERRORS_DOC, | |
342 | span, | |
343 | "docs for function returning `Result` missing `# Errors` section", | |
344 | ); | |
345 | } else { | |
346 | if_chain! { | |
347 | if let Some(body_id) = body_id; | |
348 | if let Some(future) = cx.tcx.lang_items().future_trait(); | |
349 | let typeck = cx.tcx.typeck_body(body_id); | |
350 | let body = cx.tcx.hir().body(body_id); | |
351 | let ret_ty = typeck.expr_ty(&body.value); | |
352 | if implements_trait(cx, ret_ty, future, &[]); | |
353 | if let ty::Opaque(_, subs) = ret_ty.kind(); | |
354 | if let Some(gen) = subs.types().next(); | |
355 | if let ty::Generator(_, subs, _) = gen.kind(); | |
c295e0f8 | 356 | if is_type_diagnostic_item(cx, subs.as_generator().return_ty(), sym::Result); |
f20569fa XL |
357 | then { |
358 | span_lint( | |
359 | cx, | |
360 | MISSING_ERRORS_DOC, | |
361 | span, | |
362 | "docs for function returning `Result` missing `# Errors` section", | |
363 | ); | |
364 | } | |
365 | } | |
366 | } | |
367 | } | |
368 | } | |
369 | ||
370 | /// Cleanup documentation decoration. | |
371 | /// | |
372 | /// We can't use `rustc_ast::attr::AttributeMethods::with_desugared_doc` or | |
373 | /// `rustc_ast::parse::lexer::comments::strip_doc_comment_decoration` because we | |
374 | /// need to keep track of | |
375 | /// the spans but this function is inspired from the later. | |
923072b8 | 376 | #[expect(clippy::cast_possible_truncation)] |
f20569fa XL |
377 | #[must_use] |
378 | pub fn strip_doc_comment_decoration(doc: &str, comment_kind: CommentKind, span: Span) -> (String, Vec<(usize, Span)>) { | |
379 | // one-line comments lose their prefix | |
380 | if comment_kind == CommentKind::Line { | |
381 | let mut doc = doc.to_owned(); | |
382 | doc.push('\n'); | |
383 | let len = doc.len(); | |
384 | // +3 skips the opening delimiter | |
385 | return (doc, vec![(len, span.with_lo(span.lo() + BytePos(3)))]); | |
386 | } | |
387 | ||
388 | let mut sizes = vec![]; | |
389 | let mut contains_initial_stars = false; | |
390 | for line in doc.lines() { | |
391 | let offset = line.as_ptr() as usize - doc.as_ptr() as usize; | |
392 | debug_assert_eq!(offset as u32 as usize, offset); | |
393 | contains_initial_stars |= line.trim_start().starts_with('*'); | |
394 | // +1 adds the newline, +3 skips the opening delimiter | |
395 | sizes.push((line.len() + 1, span.with_lo(span.lo() + BytePos(3 + offset as u32)))); | |
396 | } | |
397 | if !contains_initial_stars { | |
398 | return (doc.to_string(), sizes); | |
399 | } | |
400 | // remove the initial '*'s if any | |
401 | let mut no_stars = String::with_capacity(doc.len()); | |
402 | for line in doc.lines() { | |
403 | let mut chars = line.chars(); | |
17df50a5 | 404 | for c in &mut chars { |
f20569fa XL |
405 | if c.is_whitespace() { |
406 | no_stars.push(c); | |
407 | } else { | |
408 | no_stars.push(if c == '*' { ' ' } else { c }); | |
409 | break; | |
410 | } | |
411 | } | |
412 | no_stars.push_str(chars.as_str()); | |
413 | no_stars.push('\n'); | |
414 | } | |
415 | ||
416 | (no_stars, sizes) | |
417 | } | |
418 | ||
419 | #[derive(Copy, Clone)] | |
420 | struct DocHeaders { | |
421 | safety: bool, | |
422 | errors: bool, | |
423 | panics: bool, | |
424 | } | |
425 | ||
426 | fn check_attrs<'a>(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &'a [Attribute]) -> DocHeaders { | |
c295e0f8 XL |
427 | use pulldown_cmark::{BrokenLink, CowStr, Options}; |
428 | /// We don't want the parser to choke on intra doc links. Since we don't | |
429 | /// actually care about rendering them, just pretend that all broken links are | |
430 | /// point to a fake address. | |
923072b8 | 431 | #[expect(clippy::unnecessary_wraps)] // we're following a type signature |
c295e0f8 XL |
432 | fn fake_broken_link_callback<'a>(_: BrokenLink<'_>) -> Option<(CowStr<'a>, CowStr<'a>)> { |
433 | Some(("fake".into(), "fake".into())) | |
434 | } | |
435 | ||
f20569fa XL |
436 | let mut doc = String::new(); |
437 | let mut spans = vec![]; | |
438 | ||
439 | for attr in attrs { | |
440 | if let AttrKind::DocComment(comment_kind, comment) = attr.kind { | |
a2a8927a | 441 | let (comment, current_spans) = strip_doc_comment_decoration(comment.as_str(), comment_kind, attr.span); |
f20569fa XL |
442 | spans.extend_from_slice(¤t_spans); |
443 | doc.push_str(&comment); | |
444 | } else if attr.has_name(sym::doc) { | |
445 | // ignore mix of sugared and non-sugared doc | |
446 | // don't trigger the safety or errors check | |
447 | return DocHeaders { | |
448 | safety: true, | |
449 | errors: true, | |
450 | panics: true, | |
451 | }; | |
452 | } | |
453 | } | |
454 | ||
455 | let mut current = 0; | |
456 | for &mut (ref mut offset, _) in &mut spans { | |
457 | let offset_copy = *offset; | |
458 | *offset = current; | |
459 | current += offset_copy; | |
460 | } | |
461 | ||
462 | if doc.is_empty() { | |
463 | return DocHeaders { | |
464 | safety: false, | |
465 | errors: false, | |
466 | panics: false, | |
467 | }; | |
468 | } | |
469 | ||
c295e0f8 XL |
470 | let mut cb = fake_broken_link_callback; |
471 | ||
472 | let parser = | |
473 | pulldown_cmark::Parser::new_with_broken_link_callback(&doc, Options::empty(), Some(&mut cb)).into_offset_iter(); | |
f20569fa XL |
474 | // Iterate over all `Events` and combine consecutive events into one |
475 | let events = parser.coalesce(|previous, current| { | |
476 | use pulldown_cmark::Event::Text; | |
477 | ||
478 | let previous_range = previous.1; | |
479 | let current_range = current.1; | |
480 | ||
481 | match (previous.0, current.0) { | |
482 | (Text(previous), Text(current)) => { | |
483 | let mut previous = previous.to_string(); | |
484 | previous.push_str(¤t); | |
485 | Ok((Text(previous.into()), previous_range)) | |
486 | }, | |
487 | (previous, current) => Err(((previous, previous_range), (current, current_range))), | |
488 | } | |
489 | }); | |
490 | check_doc(cx, valid_idents, events, &spans) | |
491 | } | |
492 | ||
493 | const RUST_CODE: &[&str] = &["rust", "no_run", "should_panic", "compile_fail"]; | |
494 | ||
495 | fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>( | |
496 | cx: &LateContext<'_>, | |
497 | valid_idents: &FxHashSet<String>, | |
498 | events: Events, | |
499 | spans: &[(usize, Span)], | |
500 | ) -> DocHeaders { | |
501 | // true if a safety header was found | |
f20569fa XL |
502 | use pulldown_cmark::Event::{ |
503 | Code, End, FootnoteReference, HardBreak, Html, Rule, SoftBreak, Start, TaskListMarker, Text, | |
504 | }; | |
136023e0 XL |
505 | use pulldown_cmark::Tag::{CodeBlock, Heading, Item, Link, Paragraph}; |
506 | use pulldown_cmark::{CodeBlockKind, CowStr}; | |
f20569fa XL |
507 | |
508 | let mut headers = DocHeaders { | |
509 | safety: false, | |
510 | errors: false, | |
511 | panics: false, | |
512 | }; | |
513 | let mut in_code = false; | |
514 | let mut in_link = None; | |
515 | let mut in_heading = false; | |
516 | let mut is_rust = false; | |
517 | let mut edition = None; | |
136023e0 XL |
518 | let mut ticks_unbalanced = false; |
519 | let mut text_to_check: Vec<(CowStr<'_>, Span)> = Vec::new(); | |
520 | let mut paragraph_span = spans.get(0).expect("function isn't called if doc comment is empty").1; | |
f20569fa XL |
521 | for (event, range) in events { |
522 | match event { | |
523 | Start(CodeBlock(ref kind)) => { | |
524 | in_code = true; | |
525 | if let CodeBlockKind::Fenced(lang) = kind { | |
526 | for item in lang.split(',') { | |
527 | if item == "ignore" { | |
528 | is_rust = false; | |
529 | break; | |
530 | } | |
531 | if let Some(stripped) = item.strip_prefix("edition") { | |
532 | is_rust = true; | |
533 | edition = stripped.parse::<Edition>().ok(); | |
534 | } else if item.is_empty() || RUST_CODE.contains(&item) { | |
535 | is_rust = true; | |
536 | } | |
537 | } | |
538 | } | |
539 | }, | |
540 | End(CodeBlock(_)) => { | |
541 | in_code = false; | |
542 | is_rust = false; | |
543 | }, | |
544 | Start(Link(_, url, _)) => in_link = Some(url), | |
545 | End(Link(..)) => in_link = None, | |
a2a8927a XL |
546 | Start(Heading(_, _, _) | Paragraph | Item) => { |
547 | if let Start(Heading(_, _, _)) = event { | |
136023e0 XL |
548 | in_heading = true; |
549 | } | |
550 | ticks_unbalanced = false; | |
551 | let (_, span) = get_current_span(spans, range.start); | |
552 | paragraph_span = first_line_of_span(cx, span); | |
553 | }, | |
a2a8927a XL |
554 | End(Heading(_, _, _) | Paragraph | Item) => { |
555 | if let End(Heading(_, _, _)) = event { | |
136023e0 XL |
556 | in_heading = false; |
557 | } | |
558 | if ticks_unbalanced { | |
559 | span_lint_and_help( | |
560 | cx, | |
561 | DOC_MARKDOWN, | |
562 | paragraph_span, | |
563 | "backticks are unbalanced", | |
564 | None, | |
565 | "a backtick may be missing a pair", | |
566 | ); | |
567 | } else { | |
568 | for (text, span) in text_to_check { | |
569 | check_text(cx, valid_idents, &text, span); | |
570 | } | |
571 | } | |
572 | text_to_check = Vec::new(); | |
573 | }, | |
f20569fa XL |
574 | Start(_tag) | End(_tag) => (), // We don't care about other tags |
575 | Html(_html) => (), // HTML is weird, just ignore it | |
576 | SoftBreak | HardBreak | TaskListMarker(_) | Code(_) | Rule => (), | |
577 | FootnoteReference(text) | Text(text) => { | |
136023e0 XL |
578 | let (begin, span) = get_current_span(spans, range.start); |
579 | paragraph_span = paragraph_span.with_hi(span.hi()); | |
580 | ticks_unbalanced |= text.contains('`') && !in_code; | |
581 | if Some(&text) == in_link.as_ref() || ticks_unbalanced { | |
f20569fa XL |
582 | // Probably a link of the form `<http://example.com>` |
583 | // Which are represented as a link to "http://example.com" with | |
584 | // text "http://example.com" by pulldown-cmark | |
585 | continue; | |
586 | } | |
3c0e092e XL |
587 | let trimmed_text = text.trim(); |
588 | headers.safety |= in_heading && trimmed_text == "Safety"; | |
589 | headers.safety |= in_heading && trimmed_text == "Implementation safety"; | |
590 | headers.safety |= in_heading && trimmed_text == "Implementation Safety"; | |
591 | headers.errors |= in_heading && trimmed_text == "Errors"; | |
592 | headers.panics |= in_heading && trimmed_text == "Panics"; | |
f20569fa XL |
593 | if in_code { |
594 | if is_rust { | |
595 | let edition = edition.unwrap_or_else(|| cx.tcx.sess.edition()); | |
596 | check_code(cx, &text, edition, span); | |
597 | } | |
598 | } else { | |
599 | // Adjust for the beginning of the current `Event` | |
600 | let span = span.with_lo(span.lo() + BytePos::from_usize(range.start - begin)); | |
136023e0 | 601 | text_to_check.push((text, span)); |
f20569fa XL |
602 | } |
603 | }, | |
604 | } | |
605 | } | |
606 | headers | |
607 | } | |
608 | ||
136023e0 XL |
609 | fn get_current_span(spans: &[(usize, Span)], idx: usize) -> (usize, Span) { |
610 | let index = match spans.binary_search_by(|c| c.0.cmp(&idx)) { | |
611 | Ok(o) => o, | |
612 | Err(e) => e - 1, | |
613 | }; | |
614 | spans[index] | |
615 | } | |
616 | ||
f20569fa | 617 | fn check_code(cx: &LateContext<'_>, text: &str, edition: Edition, span: Span) { |
136023e0 | 618 | fn has_needless_main(code: String, edition: Edition) -> bool { |
f20569fa | 619 | rustc_driver::catch_fatal_errors(|| { |
136023e0 XL |
620 | rustc_span::create_session_globals_then(edition, || { |
621 | let filename = FileName::anon_source_code(&code); | |
f20569fa XL |
622 | |
623 | let sm = Lrc::new(SourceMap::new(FilePathMapping::empty())); | |
04454e1e FG |
624 | let fallback_bundle = |
625 | rustc_errors::fallback_fluent_bundle(rustc_errors::DEFAULT_LOCALE_RESOURCES, false); | |
626 | let emitter = EmitterWriter::new( | |
627 | Box::new(io::sink()), | |
628 | None, | |
629 | None, | |
630 | fallback_bundle, | |
631 | false, | |
632 | false, | |
633 | false, | |
634 | None, | |
635 | false, | |
636 | ); | |
94222f64 | 637 | let handler = Handler::with_emitter(false, None, Box::new(emitter)); |
f20569fa XL |
638 | let sess = ParseSess::with_span_handler(handler, sm); |
639 | ||
136023e0 | 640 | let mut parser = match maybe_new_parser_from_source_str(&sess, filename, code) { |
f20569fa XL |
641 | Ok(p) => p, |
642 | Err(errs) => { | |
5e7ed085 | 643 | drop(errs); |
f20569fa XL |
644 | return false; |
645 | }, | |
646 | }; | |
647 | ||
648 | let mut relevant_main_found = false; | |
649 | loop { | |
650 | match parser.parse_item(ForceCollect::No) { | |
651 | Ok(Some(item)) => match &item.kind { | |
a2a8927a XL |
652 | ItemKind::Fn(box Fn { |
653 | sig, body: Some(block), .. | |
654 | }) if item.ident.name == sym::main => { | |
f20569fa XL |
655 | let is_async = matches!(sig.header.asyncness, Async::Yes { .. }); |
656 | let returns_nothing = match &sig.decl.output { | |
657 | FnRetTy::Default(..) => true, | |
658 | FnRetTy::Ty(ty) if ty.kind.is_unit() => true, | |
cdc7bbd5 | 659 | FnRetTy::Ty(_) => false, |
f20569fa XL |
660 | }; |
661 | ||
662 | if returns_nothing && !is_async && !block.stmts.is_empty() { | |
663 | // This main function should be linted, but only if there are no other functions | |
664 | relevant_main_found = true; | |
665 | } else { | |
666 | // This main function should not be linted, we're done | |
667 | return false; | |
668 | } | |
669 | }, | |
5e7ed085 FG |
670 | // Tests with one of these items are ignored |
671 | ItemKind::Static(..) | |
672 | | ItemKind::Const(..) | |
673 | | ItemKind::ExternCrate(..) | |
674 | | ItemKind::ForeignMod(..) | |
675 | // Another function was found; this case is ignored | |
676 | | ItemKind::Fn(..) => return false, | |
f20569fa XL |
677 | _ => {}, |
678 | }, | |
679 | Ok(None) => break, | |
5e7ed085 | 680 | Err(e) => { |
f20569fa XL |
681 | e.cancel(); |
682 | return false; | |
683 | }, | |
684 | } | |
685 | } | |
686 | ||
687 | relevant_main_found | |
688 | }) | |
689 | }) | |
690 | .ok() | |
691 | .unwrap_or_default() | |
692 | } | |
693 | ||
136023e0 XL |
694 | // Because of the global session, we need to create a new session in a different thread with |
695 | // the edition we need. | |
696 | let text = text.to_owned(); | |
697 | if thread::spawn(move || has_needless_main(text, edition)) | |
698 | .join() | |
699 | .expect("thread::spawn failed") | |
700 | { | |
f20569fa XL |
701 | span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest"); |
702 | } | |
703 | } | |
704 | ||
705 | fn check_text(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, text: &str, span: Span) { | |
706 | for word in text.split(|c: char| c.is_whitespace() || c == '\'') { | |
707 | // Trim punctuation as in `some comment (see foo::bar).` | |
708 | // ^^ | |
3c0e092e XL |
709 | // Or even as in `_foo bar_` which is emphasized. Also preserve `::` as a prefix/suffix. |
710 | let mut word = word.trim_matches(|c: char| !c.is_alphanumeric() && c != ':'); | |
f20569fa | 711 | |
3c0e092e XL |
712 | // Remove leading or trailing single `:` which may be part of a sentence. |
713 | if word.starts_with(':') && !word.starts_with("::") { | |
714 | word = word.trim_start_matches(':'); | |
715 | } | |
716 | if word.ends_with(':') && !word.ends_with("::") { | |
717 | word = word.trim_end_matches(':'); | |
718 | } | |
719 | ||
720 | if valid_idents.contains(word) || word.chars().all(|c| c == ':') { | |
f20569fa XL |
721 | continue; |
722 | } | |
723 | ||
724 | // Adjust for the current word | |
725 | let offset = word.as_ptr() as usize - text.as_ptr() as usize; | |
726 | let span = Span::new( | |
727 | span.lo() + BytePos::from_usize(offset), | |
728 | span.lo() + BytePos::from_usize(offset + word.len()), | |
729 | span.ctxt(), | |
c295e0f8 | 730 | span.parent(), |
f20569fa XL |
731 | ); |
732 | ||
733 | check_word(cx, word, span); | |
734 | } | |
735 | } | |
736 | ||
737 | fn check_word(cx: &LateContext<'_>, word: &str, span: Span) { | |
738 | /// Checks if a string is camel-case, i.e., contains at least two uppercase | |
739 | /// letters (`Clippy` is ok) and one lower-case letter (`NASA` is ok). | |
740 | /// Plurals are also excluded (`IDs` is ok). | |
741 | fn is_camel_case(s: &str) -> bool { | |
04454e1e | 742 | if s.starts_with(|c: char| c.is_ascii_digit()) { |
f20569fa XL |
743 | return false; |
744 | } | |
745 | ||
746 | let s = s.strip_suffix('s').unwrap_or(s); | |
747 | ||
748 | s.chars().all(char::is_alphanumeric) | |
749 | && s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1 | |
750 | && s.chars().filter(|&c| c.is_lowercase()).take(1).count() > 0 | |
751 | } | |
752 | ||
753 | fn has_underscore(s: &str) -> bool { | |
754 | s != "_" && !s.contains("\\_") && s.contains('_') | |
755 | } | |
756 | ||
757 | fn has_hyphen(s: &str) -> bool { | |
758 | s != "-" && s.contains('-') | |
759 | } | |
760 | ||
761 | if let Ok(url) = Url::parse(word) { | |
762 | // try to get around the fact that `foo::bar` parses as a valid URL | |
763 | if !url.cannot_be_a_base() { | |
764 | span_lint( | |
765 | cx, | |
766 | DOC_MARKDOWN, | |
767 | span, | |
768 | "you should put bare URLs between `<`/`>` or make a proper Markdown link", | |
769 | ); | |
770 | ||
771 | return; | |
772 | } | |
773 | } | |
774 | ||
3c0e092e | 775 | // We assume that mixed-case words are not meant to be put inside backticks. (Issue #2343) |
f20569fa XL |
776 | if has_underscore(word) && has_hyphen(word) { |
777 | return; | |
778 | } | |
779 | ||
780 | if has_underscore(word) || word.contains("::") || is_camel_case(word) { | |
3c0e092e XL |
781 | let mut applicability = Applicability::MachineApplicable; |
782 | ||
a2a8927a | 783 | span_lint_and_then( |
f20569fa XL |
784 | cx, |
785 | DOC_MARKDOWN, | |
786 | span, | |
3c0e092e | 787 | "item in documentation is missing backticks", |
a2a8927a XL |
788 | |diag| { |
789 | let snippet = snippet_with_applicability(cx, span, "..", &mut applicability); | |
790 | diag.span_suggestion_with_style( | |
791 | span, | |
792 | "try", | |
793 | format!("`{}`", snippet), | |
794 | applicability, | |
795 | // always show the suggestion in a separate line, since the | |
796 | // inline presentation adds another pair of backticks | |
797 | SuggestionStyle::ShowAlways, | |
798 | ); | |
799 | }, | |
f20569fa XL |
800 | ); |
801 | } | |
802 | } | |
803 | ||
804 | struct FindPanicUnwrap<'a, 'tcx> { | |
805 | cx: &'a LateContext<'tcx>, | |
806 | panic_span: Option<Span>, | |
807 | typeck_results: &'tcx ty::TypeckResults<'tcx>, | |
808 | } | |
809 | ||
810 | impl<'a, 'tcx> Visitor<'tcx> for FindPanicUnwrap<'a, 'tcx> { | |
5099ac24 | 811 | type NestedFilter = nested_filter::OnlyBodies; |
f20569fa XL |
812 | |
813 | fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { | |
814 | if self.panic_span.is_some() { | |
815 | return; | |
816 | } | |
817 | ||
5099ac24 FG |
818 | if let Some(macro_call) = root_macro_call_first_node(self.cx, expr) { |
819 | if is_panic(self.cx, macro_call.def_id) | |
820 | || matches!( | |
821 | self.cx.tcx.item_name(macro_call.def_id).as_str(), | |
822 | "assert" | "assert_eq" | "assert_ne" | "todo" | |
823 | ) | |
824 | { | |
825 | self.panic_span = Some(macro_call.span); | |
f20569fa XL |
826 | } |
827 | } | |
828 | ||
829 | // check for `unwrap` | |
830 | if let Some(arglists) = method_chain_args(expr, &["unwrap"]) { | |
3c0e092e XL |
831 | let receiver_ty = self.typeck_results.expr_ty(&arglists[0][0]).peel_refs(); |
832 | if is_type_diagnostic_item(self.cx, receiver_ty, sym::Option) | |
833 | || is_type_diagnostic_item(self.cx, receiver_ty, sym::Result) | |
f20569fa XL |
834 | { |
835 | self.panic_span = Some(expr.span); | |
836 | } | |
837 | } | |
838 | ||
839 | // and check sub-expressions | |
840 | intravisit::walk_expr(self, expr); | |
841 | } | |
842 | ||
cdc7bbd5 XL |
843 | // Panics in const blocks will cause compilation to fail. |
844 | fn visit_anon_const(&mut self, _: &'tcx AnonConst) {} | |
845 | ||
5099ac24 FG |
846 | fn nested_visit_map(&mut self) -> Self::Map { |
847 | self.cx.tcx.hir() | |
f20569fa XL |
848 | } |
849 | } |