]> git.lizzy.rs Git - rust.git/blob - declare_clippy_lint/src/lib.rs
Auto merge of #9648 - llogiq:fix-undocumented-unsafe-blocks, r=Jarcho
[rust.git] / declare_clippy_lint / src / lib.rs
1 #![feature(let_chains)]
2 #![cfg_attr(feature = "deny-warnings", deny(warnings))]
3
4 use proc_macro::TokenStream;
5 use quote::{format_ident, quote};
6 use syn::parse::{Parse, ParseStream};
7 use syn::{parse_macro_input, Attribute, Error, Ident, Lit, LitStr, Meta, Result, Token};
8
9 fn parse_attr<const LEN: usize>(path: [&'static str; LEN], attr: &Attribute) -> Option<LitStr> {
10     if let Meta::NameValue(name_value) = attr.parse_meta().ok()? {
11         let path_idents = name_value.path.segments.iter().map(|segment| &segment.ident);
12
13         if itertools::equal(path_idents, path)
14             && let Lit::Str(lit) = name_value.lit
15         {
16             return Some(lit);
17         }
18     }
19
20     None
21 }
22
23 struct ClippyLint {
24     attrs: Vec<Attribute>,
25     explanation: String,
26     name: Ident,
27     category: Ident,
28     description: LitStr,
29 }
30
31 impl Parse for ClippyLint {
32     fn parse(input: ParseStream) -> Result<Self> {
33         let attrs = input.call(Attribute::parse_outer)?;
34
35         let mut in_code = false;
36         let mut explanation = String::new();
37         let mut version = None;
38         for attr in &attrs {
39             if let Some(lit) = parse_attr(["doc"], attr) {
40                 let value = lit.value();
41                 let line = value.strip_prefix(' ').unwrap_or(&value);
42
43                 if line.starts_with("```") {
44                     explanation += "```\n";
45                     in_code = !in_code;
46                 } else if !(in_code && line.starts_with("# ")) {
47                     explanation += line;
48                     explanation.push('\n');
49                 }
50             } else if let Some(lit) = parse_attr(["clippy", "version"], attr) {
51                 if let Some(duplicate) = version.replace(lit) {
52                     return Err(Error::new_spanned(duplicate, "duplicate clippy::version"));
53                 }
54             } else {
55                 return Err(Error::new_spanned(attr, "unexpected attribute"));
56             }
57         }
58
59         input.parse::<Token![pub]>()?;
60         let name = input.parse()?;
61         input.parse::<Token![,]>()?;
62
63         let category = input.parse()?;
64         input.parse::<Token![,]>()?;
65
66         let description = input.parse()?;
67
68         Ok(Self {
69             attrs,
70             explanation,
71             name,
72             category,
73             description,
74         })
75     }
76 }
77
78 /// Macro used to declare a Clippy lint.
79 ///
80 /// Every lint declaration consists of 4 parts:
81 ///
82 /// 1. The documentation, which is used for the website and `cargo clippy --explain`
83 /// 2. The `LINT_NAME`. See [lint naming][lint_naming] on lint naming conventions.
84 /// 3. The `lint_level`, which is a mapping from *one* of our lint groups to `Allow`, `Warn` or
85 ///    `Deny`. The lint level here has nothing to do with what lint groups the lint is a part of.
86 /// 4. The `description` that contains a short explanation on what's wrong with code where the
87 ///    lint is triggered.
88 ///
89 /// Currently the categories `style`, `correctness`, `suspicious`, `complexity` and `perf` are
90 /// enabled by default. As said in the README.md of this repository, if the lint level mapping
91 /// changes, please update README.md.
92 ///
93 /// # Example
94 ///
95 /// ```
96 /// use rustc_session::declare_tool_lint;
97 ///
98 /// declare_clippy_lint! {
99 ///     /// ### What it does
100 ///     /// Checks for ... (describe what the lint matches).
101 ///     ///
102 ///     /// ### Why is this bad?
103 ///     /// Supply the reason for linting the code.
104 ///     ///
105 ///     /// ### Example
106 ///     /// ```rust
107 ///     /// Insert a short example of code that triggers the lint
108 ///     /// ```
109 ///     ///
110 ///     /// Use instead:
111 ///     /// ```rust
112 ///     /// Insert a short example of improved code that doesn't trigger the lint
113 ///     /// ```
114 ///     #[clippy::version = "1.65.0"]
115 ///     pub LINT_NAME,
116 ///     pedantic,
117 ///     "description"
118 /// }
119 /// ```
120 /// [lint_naming]: https://rust-lang.github.io/rfcs/0344-conventions-galore.html#lints
121 #[proc_macro]
122 pub fn declare_clippy_lint(input: TokenStream) -> TokenStream {
123     let ClippyLint {
124         attrs,
125         explanation,
126         name,
127         category,
128         description,
129     } = parse_macro_input!(input as ClippyLint);
130
131     let mut category = category.to_string();
132
133     let level = format_ident!(
134         "{}",
135         match category.as_str() {
136             "correctness" => "Deny",
137             "style" | "suspicious" | "complexity" | "perf" | "internal_warn" => "Warn",
138             "pedantic" | "restriction" | "cargo" | "nursery" | "internal" => "Allow",
139             _ => panic!("unknown category {category}"),
140         },
141     );
142
143     let info = if category == "internal_warn" {
144         None
145     } else {
146         let info_name = format_ident!("{name}_INFO");
147
148         (&mut category[0..1]).make_ascii_uppercase();
149         let category_variant = format_ident!("{category}");
150
151         Some(quote! {
152             pub(crate) static #info_name: &'static crate::LintInfo = &crate::LintInfo {
153                 lint: &#name,
154                 category: crate::LintCategory::#category_variant,
155                 explanation: #explanation,
156             };
157         })
158     };
159
160     let output = quote! {
161         declare_tool_lint! {
162             #(#attrs)*
163             pub clippy::#name,
164             #level,
165             #description,
166             report_in_external_macro: true
167         }
168
169         #info
170     };
171
172     TokenStream::from(output)
173 }