]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/attrs.rs
Rollup merge of #83092 - petrochenkov:qspan, r=estebank
[rust.git] / clippy_lints / src / attrs.rs
1 //! checks for attributes
2
3 use crate::utils::{
4     first_line_of_span, is_present_in_source, match_panic_def_id, snippet_opt, span_lint, span_lint_and_help,
5     span_lint_and_sugg, span_lint_and_then, without_block_comments,
6 };
7 use if_chain::if_chain;
8 use rustc_ast::{AttrKind, AttrStyle, Attribute, Lit, LitKind, MetaItemKind, NestedMetaItem};
9 use rustc_errors::Applicability;
10 use rustc_hir::{
11     Block, Expr, ExprKind, ImplItem, ImplItemKind, Item, ItemKind, StmtKind, TraitFn, TraitItem, TraitItemKind,
12 };
13 use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext};
14 use rustc_middle::lint::in_external_macro;
15 use rustc_middle::ty;
16 use rustc_session::{declare_lint_pass, declare_tool_lint};
17 use rustc_span::source_map::Span;
18 use rustc_span::sym;
19 use rustc_span::symbol::{Symbol, SymbolStr};
20 use semver::Version;
21
22 static UNIX_SYSTEMS: &[&str] = &[
23     "android",
24     "dragonfly",
25     "emscripten",
26     "freebsd",
27     "fuchsia",
28     "haiku",
29     "illumos",
30     "ios",
31     "l4re",
32     "linux",
33     "macos",
34     "netbsd",
35     "openbsd",
36     "redox",
37     "solaris",
38     "vxworks",
39 ];
40
41 // NOTE: windows is excluded from the list because it's also a valid target family.
42 static NON_UNIX_SYSTEMS: &[&str] = &["hermit", "none", "wasi"];
43
44 declare_clippy_lint! {
45     /// **What it does:** Checks for items annotated with `#[inline(always)]`,
46     /// unless the annotated function is empty or simply panics.
47     ///
48     /// **Why is this bad?** While there are valid uses of this annotation (and once
49     /// you know when to use it, by all means `allow` this lint), it's a common
50     /// newbie-mistake to pepper one's code with it.
51     ///
52     /// As a rule of thumb, before slapping `#[inline(always)]` on a function,
53     /// measure if that additional function call really affects your runtime profile
54     /// sufficiently to make up for the increase in compile time.
55     ///
56     /// **Known problems:** False positives, big time. This lint is meant to be
57     /// deactivated by everyone doing serious performance work. This means having
58     /// done the measurement.
59     ///
60     /// **Example:**
61     /// ```ignore
62     /// #[inline(always)]
63     /// fn not_quite_hot_code(..) { ... }
64     /// ```
65     pub INLINE_ALWAYS,
66     pedantic,
67     "use of `#[inline(always)]`"
68 }
69
70 declare_clippy_lint! {
71     /// **What it does:** Checks for `extern crate` and `use` items annotated with
72     /// lint attributes.
73     ///
74     /// This lint permits `#[allow(unused_imports)]`, `#[allow(deprecated)]`,
75     /// `#[allow(unreachable_pub)]`, `#[allow(clippy::wildcard_imports)]` and
76     /// `#[allow(clippy::enum_glob_use)]` on `use` items and `#[allow(unused_imports)]` on
77     /// `extern crate` items with a `#[macro_use]` attribute.
78     ///
79     /// **Why is this bad?** Lint attributes have no effect on crate imports. Most
80     /// likely a `!` was forgotten.
81     ///
82     /// **Known problems:** None.
83     ///
84     /// **Example:**
85     /// ```ignore
86     /// // Bad
87     /// #[deny(dead_code)]
88     /// extern crate foo;
89     /// #[forbid(dead_code)]
90     /// use foo::bar;
91     ///
92     /// // Ok
93     /// #[allow(unused_imports)]
94     /// use foo::baz;
95     /// #[allow(unused_imports)]
96     /// #[macro_use]
97     /// extern crate baz;
98     /// ```
99     pub USELESS_ATTRIBUTE,
100     correctness,
101     "use of lint attributes on `extern crate` items"
102 }
103
104 declare_clippy_lint! {
105     /// **What it does:** Checks for `#[deprecated]` annotations with a `since`
106     /// field that is not a valid semantic version.
107     ///
108     /// **Why is this bad?** For checking the version of the deprecation, it must be
109     /// a valid semver. Failing that, the contained information is useless.
110     ///
111     /// **Known problems:** None.
112     ///
113     /// **Example:**
114     /// ```rust
115     /// #[deprecated(since = "forever")]
116     /// fn something_else() { /* ... */ }
117     /// ```
118     pub DEPRECATED_SEMVER,
119     correctness,
120     "use of `#[deprecated(since = \"x\")]` where x is not semver"
121 }
122
123 declare_clippy_lint! {
124     /// **What it does:** Checks for empty lines after outer attributes
125     ///
126     /// **Why is this bad?**
127     /// Most likely the attribute was meant to be an inner attribute using a '!'.
128     /// If it was meant to be an outer attribute, then the following item
129     /// should not be separated by empty lines.
130     ///
131     /// **Known problems:** Can cause false positives.
132     ///
133     /// From the clippy side it's difficult to detect empty lines between an attributes and the
134     /// following item because empty lines and comments are not part of the AST. The parsing
135     /// currently works for basic cases but is not perfect.
136     ///
137     /// **Example:**
138     /// ```rust
139     /// // Good (as inner attribute)
140     /// #![allow(dead_code)]
141     ///
142     /// fn this_is_fine() { }
143     ///
144     /// // Bad
145     /// #[allow(dead_code)]
146     ///
147     /// fn not_quite_good_code() { }
148     ///
149     /// // Good (as outer attribute)
150     /// #[allow(dead_code)]
151     /// fn this_is_fine_too() { }
152     /// ```
153     pub EMPTY_LINE_AFTER_OUTER_ATTR,
154     nursery,
155     "empty line after outer attribute"
156 }
157
158 declare_clippy_lint! {
159     /// **What it does:** Checks for `warn`/`deny`/`forbid` attributes targeting the whole clippy::restriction category.
160     ///
161     /// **Why is this bad?** Restriction lints sometimes are in contrast with other lints or even go against idiomatic rust.
162     /// These lints should only be enabled on a lint-by-lint basis and with careful consideration.
163     ///
164     /// **Known problems:** None.
165     ///
166     /// **Example:**
167     /// Bad:
168     /// ```rust
169     /// #![deny(clippy::restriction)]
170     /// ```
171     ///
172     /// Good:
173     /// ```rust
174     /// #![deny(clippy::as_conversions)]
175     /// ```
176     pub BLANKET_CLIPPY_RESTRICTION_LINTS,
177     style,
178     "enabling the complete restriction group"
179 }
180
181 declare_clippy_lint! {
182     /// **What it does:** Checks for `#[cfg_attr(rustfmt, rustfmt_skip)]` and suggests to replace it
183     /// with `#[rustfmt::skip]`.
184     ///
185     /// **Why is this bad?** Since tool_attributes ([rust-lang/rust#44690](https://github.com/rust-lang/rust/issues/44690))
186     /// are stable now, they should be used instead of the old `cfg_attr(rustfmt)` attributes.
187     ///
188     /// **Known problems:** This lint doesn't detect crate level inner attributes, because they get
189     /// processed before the PreExpansionPass lints get executed. See
190     /// [#3123](https://github.com/rust-lang/rust-clippy/pull/3123#issuecomment-422321765)
191     ///
192     /// **Example:**
193     ///
194     /// Bad:
195     /// ```rust
196     /// #[cfg_attr(rustfmt, rustfmt_skip)]
197     /// fn main() { }
198     /// ```
199     ///
200     /// Good:
201     /// ```rust
202     /// #[rustfmt::skip]
203     /// fn main() { }
204     /// ```
205     pub DEPRECATED_CFG_ATTR,
206     complexity,
207     "usage of `cfg_attr(rustfmt)` instead of tool attributes"
208 }
209
210 declare_clippy_lint! {
211     /// **What it does:** Checks for cfg attributes having operating systems used in target family position.
212     ///
213     /// **Why is this bad?** The configuration option will not be recognised and the related item will not be included
214     /// by the conditional compilation engine.
215     ///
216     /// **Known problems:** None.
217     ///
218     /// **Example:**
219     ///
220     /// Bad:
221     /// ```rust
222     /// #[cfg(linux)]
223     /// fn conditional() { }
224     /// ```
225     ///
226     /// Good:
227     /// ```rust
228     /// #[cfg(target_os = "linux")]
229     /// fn conditional() { }
230     /// ```
231     ///
232     /// Or:
233     /// ```rust
234     /// #[cfg(unix)]
235     /// fn conditional() { }
236     /// ```
237     /// Check the [Rust Reference](https://doc.rust-lang.org/reference/conditional-compilation.html#target_os) for more details.
238     pub MISMATCHED_TARGET_OS,
239     correctness,
240     "usage of `cfg(operating_system)` instead of `cfg(target_os = \"operating_system\")`"
241 }
242
243 declare_lint_pass!(Attributes => [
244     INLINE_ALWAYS,
245     DEPRECATED_SEMVER,
246     USELESS_ATTRIBUTE,
247     BLANKET_CLIPPY_RESTRICTION_LINTS,
248 ]);
249
250 impl<'tcx> LateLintPass<'tcx> for Attributes {
251     fn check_attribute(&mut self, cx: &LateContext<'tcx>, attr: &'tcx Attribute) {
252         if let Some(items) = &attr.meta_item_list() {
253             if let Some(ident) = attr.ident() {
254                 let ident = &*ident.as_str();
255                 match ident {
256                     "allow" | "warn" | "deny" | "forbid" => {
257                         check_clippy_lint_names(cx, ident, items);
258                     },
259                     _ => {},
260                 }
261                 if items.is_empty() || !attr.has_name(sym::deprecated) {
262                     return;
263                 }
264                 for item in items {
265                     if_chain! {
266                         if let NestedMetaItem::MetaItem(mi) = &item;
267                         if let MetaItemKind::NameValue(lit) = &mi.kind;
268                         if mi.has_name(sym::since);
269                         then {
270                             check_semver(cx, item.span(), lit);
271                         }
272                     }
273                 }
274             }
275         }
276     }
277
278     fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) {
279         let attrs = cx.tcx.hir().attrs(item.hir_id());
280         if is_relevant_item(cx, item) {
281             check_attrs(cx, item.span, item.ident.name, attrs)
282         }
283         match item.kind {
284             ItemKind::ExternCrate(..) | ItemKind::Use(..) => {
285                 let skip_unused_imports = attrs.iter().any(|attr| attr.has_name(sym::macro_use));
286
287                 for attr in attrs {
288                     if in_external_macro(cx.sess(), attr.span) {
289                         return;
290                     }
291                     if let Some(lint_list) = &attr.meta_item_list() {
292                         if let Some(ident) = attr.ident() {
293                             match &*ident.as_str() {
294                                 "allow" | "warn" | "deny" | "forbid" => {
295                                     // permit `unused_imports`, `deprecated`, `unreachable_pub`,
296                                     // `clippy::wildcard_imports`, and `clippy::enum_glob_use` for `use` items
297                                     // and `unused_imports` for `extern crate` items with `macro_use`
298                                     for lint in lint_list {
299                                         match item.kind {
300                                             ItemKind::Use(..) => {
301                                                 if is_word(lint, sym!(unused_imports))
302                                                     || is_word(lint, sym::deprecated)
303                                                     || is_word(lint, sym!(unreachable_pub))
304                                                     || is_word(lint, sym!(unused))
305                                                     || extract_clippy_lint(lint)
306                                                         .map_or(false, |s| s == "wildcard_imports")
307                                                     || extract_clippy_lint(lint).map_or(false, |s| s == "enum_glob_use")
308                                                 {
309                                                     return;
310                                                 }
311                                             },
312                                             ItemKind::ExternCrate(..) => {
313                                                 if is_word(lint, sym!(unused_imports)) && skip_unused_imports {
314                                                     return;
315                                                 }
316                                                 if is_word(lint, sym!(unused_extern_crates)) {
317                                                     return;
318                                                 }
319                                             },
320                                             _ => {},
321                                         }
322                                     }
323                                     let line_span = first_line_of_span(cx, attr.span);
324
325                                     if let Some(mut sugg) = snippet_opt(cx, line_span) {
326                                         if sugg.contains("#[") {
327                                             span_lint_and_then(
328                                                 cx,
329                                                 USELESS_ATTRIBUTE,
330                                                 line_span,
331                                                 "useless lint attribute",
332                                                 |diag| {
333                                                     sugg = sugg.replacen("#[", "#![", 1);
334                                                     diag.span_suggestion(
335                                                         line_span,
336                                                         "if you just forgot a `!`, use",
337                                                         sugg,
338                                                         Applicability::MaybeIncorrect,
339                                                     );
340                                                 },
341                                             );
342                                         }
343                                     }
344                                 },
345                                 _ => {},
346                             }
347                         }
348                     }
349                 }
350             },
351             _ => {},
352         }
353     }
354
355     fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx ImplItem<'_>) {
356         if is_relevant_impl(cx, item) {
357             check_attrs(cx, item.span, item.ident.name, cx.tcx.hir().attrs(item.hir_id()))
358         }
359     }
360
361     fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'_>) {
362         if is_relevant_trait(cx, item) {
363             check_attrs(cx, item.span, item.ident.name, cx.tcx.hir().attrs(item.hir_id()))
364         }
365     }
366 }
367
368 /// Returns the lint name if it is clippy lint.
369 fn extract_clippy_lint(lint: &NestedMetaItem) -> Option<SymbolStr> {
370     if_chain! {
371         if let Some(meta_item) = lint.meta_item();
372         if meta_item.path.segments.len() > 1;
373         if let tool_name = meta_item.path.segments[0].ident;
374         if tool_name.name == sym::clippy;
375         let lint_name = meta_item.path.segments.last().unwrap().ident.name;
376         then {
377             return Some(lint_name.as_str());
378         }
379     }
380     None
381 }
382
383 fn check_clippy_lint_names(cx: &LateContext<'_>, ident: &str, items: &[NestedMetaItem]) {
384     for lint in items {
385         if let Some(lint_name) = extract_clippy_lint(lint) {
386             if lint_name == "restriction" && ident != "allow" {
387                 span_lint_and_help(
388                     cx,
389                     BLANKET_CLIPPY_RESTRICTION_LINTS,
390                     lint.span(),
391                     "restriction lints are not meant to be all enabled",
392                     None,
393                     "try enabling only the lints you really need",
394                 );
395             }
396         }
397     }
398 }
399
400 fn is_relevant_item(cx: &LateContext<'_>, item: &Item<'_>) -> bool {
401     if let ItemKind::Fn(_, _, eid) = item.kind {
402         is_relevant_expr(cx, cx.tcx.typeck_body(eid), &cx.tcx.hir().body(eid).value)
403     } else {
404         true
405     }
406 }
407
408 fn is_relevant_impl(cx: &LateContext<'_>, item: &ImplItem<'_>) -> bool {
409     match item.kind {
410         ImplItemKind::Fn(_, eid) => is_relevant_expr(cx, cx.tcx.typeck_body(eid), &cx.tcx.hir().body(eid).value),
411         _ => false,
412     }
413 }
414
415 fn is_relevant_trait(cx: &LateContext<'_>, item: &TraitItem<'_>) -> bool {
416     match item.kind {
417         TraitItemKind::Fn(_, TraitFn::Required(_)) => true,
418         TraitItemKind::Fn(_, TraitFn::Provided(eid)) => {
419             is_relevant_expr(cx, cx.tcx.typeck_body(eid), &cx.tcx.hir().body(eid).value)
420         },
421         _ => false,
422     }
423 }
424
425 fn is_relevant_block(cx: &LateContext<'_>, typeck_results: &ty::TypeckResults<'_>, block: &Block<'_>) -> bool {
426     block.stmts.first().map_or(
427         block
428             .expr
429             .as_ref()
430             .map_or(false, |e| is_relevant_expr(cx, typeck_results, e)),
431         |stmt| match &stmt.kind {
432             StmtKind::Local(_) => true,
433             StmtKind::Expr(expr) | StmtKind::Semi(expr) => is_relevant_expr(cx, typeck_results, expr),
434             _ => false,
435         },
436     )
437 }
438
439 fn is_relevant_expr(cx: &LateContext<'_>, typeck_results: &ty::TypeckResults<'_>, expr: &Expr<'_>) -> bool {
440     match &expr.kind {
441         ExprKind::Block(block, _) => is_relevant_block(cx, typeck_results, block),
442         ExprKind::Ret(Some(e)) => is_relevant_expr(cx, typeck_results, e),
443         ExprKind::Ret(None) | ExprKind::Break(_, None) => false,
444         ExprKind::Call(path_expr, _) => {
445             if let ExprKind::Path(qpath) = &path_expr.kind {
446                 typeck_results
447                     .qpath_res(qpath, path_expr.hir_id)
448                     .opt_def_id()
449                     .map_or(true, |fun_id| !match_panic_def_id(cx, fun_id))
450             } else {
451                 true
452             }
453         },
454         _ => true,
455     }
456 }
457
458 fn check_attrs(cx: &LateContext<'_>, span: Span, name: Symbol, attrs: &[Attribute]) {
459     if span.from_expansion() {
460         return;
461     }
462
463     for attr in attrs {
464         if let Some(values) = attr.meta_item_list() {
465             if values.len() != 1 || !attr.has_name(sym::inline) {
466                 continue;
467             }
468             if is_word(&values[0], sym::always) {
469                 span_lint(
470                     cx,
471                     INLINE_ALWAYS,
472                     attr.span,
473                     &format!(
474                         "you have declared `#[inline(always)]` on `{}`. This is usually a bad idea",
475                         name
476                     ),
477                 );
478             }
479         }
480     }
481 }
482
483 fn check_semver(cx: &LateContext<'_>, span: Span, lit: &Lit) {
484     if let LitKind::Str(is, _) = lit.kind {
485         if Version::parse(&is.as_str()).is_ok() {
486             return;
487         }
488     }
489     span_lint(
490         cx,
491         DEPRECATED_SEMVER,
492         span,
493         "the since field must contain a semver-compliant version",
494     );
495 }
496
497 fn is_word(nmi: &NestedMetaItem, expected: Symbol) -> bool {
498     if let NestedMetaItem::MetaItem(mi) = &nmi {
499         mi.is_word() && mi.has_name(expected)
500     } else {
501         false
502     }
503 }
504
505 declare_lint_pass!(EarlyAttributes => [
506     DEPRECATED_CFG_ATTR,
507     MISMATCHED_TARGET_OS,
508     EMPTY_LINE_AFTER_OUTER_ATTR,
509 ]);
510
511 impl EarlyLintPass for EarlyAttributes {
512     fn check_item(&mut self, cx: &EarlyContext<'_>, item: &rustc_ast::Item) {
513         check_empty_line_after_outer_attr(cx, item);
514     }
515
516     fn check_attribute(&mut self, cx: &EarlyContext<'_>, attr: &Attribute) {
517         check_deprecated_cfg_attr(cx, attr);
518         check_mismatched_target_os(cx, attr);
519     }
520 }
521
522 fn check_empty_line_after_outer_attr(cx: &EarlyContext<'_>, item: &rustc_ast::Item) {
523     for attr in &item.attrs {
524         let attr_item = if let AttrKind::Normal(ref attr, _) = attr.kind {
525             attr
526         } else {
527             return;
528         };
529
530         if attr.style == AttrStyle::Outer {
531             if attr_item.args.inner_tokens().is_empty() || !is_present_in_source(cx, attr.span) {
532                 return;
533             }
534
535             let begin_of_attr_to_item = Span::new(attr.span.lo(), item.span.lo(), item.span.ctxt());
536             let end_of_attr_to_item = Span::new(attr.span.hi(), item.span.lo(), item.span.ctxt());
537
538             if let Some(snippet) = snippet_opt(cx, end_of_attr_to_item) {
539                 let lines = snippet.split('\n').collect::<Vec<_>>();
540                 let lines = without_block_comments(lines);
541
542                 if lines.iter().filter(|l| l.trim().is_empty()).count() > 2 {
543                     span_lint(
544                         cx,
545                         EMPTY_LINE_AFTER_OUTER_ATTR,
546                         begin_of_attr_to_item,
547                         "found an empty line after an outer attribute. \
548                         Perhaps you forgot to add a `!` to make it an inner attribute?",
549                     );
550                 }
551             }
552         }
553     }
554 }
555
556 fn check_deprecated_cfg_attr(cx: &EarlyContext<'_>, attr: &Attribute) {
557     if_chain! {
558         // check cfg_attr
559         if attr.has_name(sym::cfg_attr);
560         if let Some(items) = attr.meta_item_list();
561         if items.len() == 2;
562         // check for `rustfmt`
563         if let Some(feature_item) = items[0].meta_item();
564         if feature_item.has_name(sym::rustfmt);
565         // check for `rustfmt_skip` and `rustfmt::skip`
566         if let Some(skip_item) = &items[1].meta_item();
567         if skip_item.has_name(sym!(rustfmt_skip)) ||
568             skip_item.path.segments.last().expect("empty path in attribute").ident.name == sym!(skip);
569         // Only lint outer attributes, because custom inner attributes are unstable
570         // Tracking issue: https://github.com/rust-lang/rust/issues/54726
571         if let AttrStyle::Outer = attr.style;
572         then {
573             span_lint_and_sugg(
574                 cx,
575                 DEPRECATED_CFG_ATTR,
576                 attr.span,
577                 "`cfg_attr` is deprecated for rustfmt and got replaced by tool attributes",
578                 "use",
579                 "#[rustfmt::skip]".to_string(),
580                 Applicability::MachineApplicable,
581             );
582         }
583     }
584 }
585
586 fn check_mismatched_target_os(cx: &EarlyContext<'_>, attr: &Attribute) {
587     fn find_os(name: &str) -> Option<&'static str> {
588         UNIX_SYSTEMS
589             .iter()
590             .chain(NON_UNIX_SYSTEMS.iter())
591             .find(|&&os| os == name)
592             .copied()
593     }
594
595     fn is_unix(name: &str) -> bool {
596         UNIX_SYSTEMS.iter().any(|&os| os == name)
597     }
598
599     fn find_mismatched_target_os(items: &[NestedMetaItem]) -> Vec<(&str, Span)> {
600         let mut mismatched = Vec::new();
601
602         for item in items {
603             if let NestedMetaItem::MetaItem(meta) = item {
604                 match &meta.kind {
605                     MetaItemKind::List(list) => {
606                         mismatched.extend(find_mismatched_target_os(&list));
607                     },
608                     MetaItemKind::Word => {
609                         if_chain! {
610                             if let Some(ident) = meta.ident();
611                             if let Some(os) = find_os(&*ident.name.as_str());
612                             then {
613                                 mismatched.push((os, ident.span));
614                             }
615                         }
616                     },
617                     _ => {},
618                 }
619             }
620         }
621
622         mismatched
623     }
624
625     if_chain! {
626         if attr.has_name(sym::cfg);
627         if let Some(list) = attr.meta_item_list();
628         let mismatched = find_mismatched_target_os(&list);
629         if !mismatched.is_empty();
630         then {
631             let mess = "operating system used in target family position";
632
633             span_lint_and_then(cx, MISMATCHED_TARGET_OS, attr.span, &mess, |diag| {
634                 // Avoid showing the unix suggestion multiple times in case
635                 // we have more than one mismatch for unix-like systems
636                 let mut unix_suggested = false;
637
638                 for (os, span) in mismatched {
639                     let sugg = format!("target_os = \"{}\"", os);
640                     diag.span_suggestion(span, "try", sugg, Applicability::MaybeIncorrect);
641
642                     if !unix_suggested && is_unix(os) {
643                         diag.help("did you mean `unix`?");
644                         unix_suggested = true;
645                     }
646                 }
647             });
648         }
649     }
650 }