]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/utils/internal_lints/metadata_collector.rs
99e9e3275ab53e631bc7c99741e1d0ba359f4870
[rust.git] / src / tools / clippy / clippy_lints / src / utils / internal_lints / metadata_collector.rs
1 //! This lint is used to collect metadata about clippy lints. This metadata is exported as a json
2 //! file and then used to generate the [clippy lint list](https://rust-lang.github.io/rust-clippy/master/index.html)
3 //!
4 //! This module and therefore the entire lint is guarded by a feature flag called `internal`
5 //!
6 //! The module transforms all lint names to ascii lowercase to ensure that we don't have mismatches
7 //! during any comparison or mapping. (Please take care of this, it's not fun to spend time on such
8 //! a simple mistake)
9
10 use crate::renamed_lints::RENAMED_LINTS;
11 use crate::utils::internal_lints::{extract_clippy_version_value, is_lint_ref_type};
12
13 use clippy_utils::diagnostics::span_lint;
14 use clippy_utils::ty::{match_type, walk_ptrs_ty_depth};
15 use clippy_utils::{last_path_segment, match_def_path, match_function_call, match_path, paths};
16 use if_chain::if_chain;
17 use rustc_ast as ast;
18 use rustc_data_structures::fx::FxHashMap;
19 use rustc_hir::{
20     self as hir, def::DefKind, intravisit, intravisit::Visitor, ExprKind, Item, ItemKind, Mutability, QPath,
21 };
22 use rustc_lint::{CheckLintNameResult, LateContext, LateLintPass, LintContext, LintId};
23 use rustc_middle::hir::nested_filter;
24 use rustc_session::{declare_tool_lint, impl_lint_pass};
25 use rustc_span::symbol::Ident;
26 use rustc_span::{sym, Loc, Span, Symbol};
27 use serde::{ser::SerializeStruct, Serialize, Serializer};
28 use std::collections::BinaryHeap;
29 use std::fmt;
30 use std::fmt::Write as _;
31 use std::fs::{self, OpenOptions};
32 use std::io::prelude::*;
33 use std::path::Path;
34 use std::path::PathBuf;
35 use std::process::Command;
36
37 /// This is the output file of the lint collector.
38 const OUTPUT_FILE: &str = "../util/gh-pages/lints.json";
39 /// These lints are excluded from the export.
40 const BLACK_LISTED_LINTS: &[&str] = &["lint_author", "dump_hir", "internal_metadata_collector"];
41 /// These groups will be ignored by the lint group matcher. This is useful for collections like
42 /// `clippy::all`
43 const IGNORED_LINT_GROUPS: [&str; 1] = ["clippy::all"];
44 /// Lints within this group will be excluded from the collection. These groups
45 /// have to be defined without the `clippy::` prefix.
46 const EXCLUDED_LINT_GROUPS: [&str; 1] = ["internal"];
47 /// Collected deprecated lint will be assigned to this group in the JSON output
48 const DEPRECATED_LINT_GROUP_STR: &str = "deprecated";
49 /// This is the lint level for deprecated lints that will be displayed in the lint list
50 const DEPRECATED_LINT_LEVEL: &str = "none";
51 /// This array holds Clippy's lint groups with their corresponding default lint level. The
52 /// lint level for deprecated lints is set in `DEPRECATED_LINT_LEVEL`.
53 const DEFAULT_LINT_LEVELS: &[(&str, &str)] = &[
54     ("correctness", "deny"),
55     ("suspicious", "warn"),
56     ("restriction", "allow"),
57     ("style", "warn"),
58     ("pedantic", "allow"),
59     ("complexity", "warn"),
60     ("perf", "warn"),
61     ("cargo", "allow"),
62     ("nursery", "allow"),
63 ];
64 /// This prefix is in front of the lint groups in the lint store. The prefix will be trimmed
65 /// to only keep the actual lint group in the output.
66 const CLIPPY_LINT_GROUP_PREFIX: &str = "clippy::";
67
68 /// This template will be used to format the configuration section in the lint documentation.
69 /// The `configurations` parameter will be replaced with one or multiple formatted
70 /// `ClippyConfiguration` instances. See `CONFIGURATION_VALUE_TEMPLATE` for further customizations
71 macro_rules! CONFIGURATION_SECTION_TEMPLATE {
72     () => {
73         r#"
74 ### Configuration
75 This lint has the following configuration variables:
76
77 {configurations}
78 "#
79     };
80 }
81 /// This template will be used to format an individual `ClippyConfiguration` instance in the
82 /// lint documentation.
83 ///
84 /// The format function will provide strings for the following parameters: `name`, `ty`, `doc` and
85 /// `default`
86 macro_rules! CONFIGURATION_VALUE_TEMPLATE {
87     () => {
88         "* `{name}`: `{ty}`: {doc} (defaults to `{default}`)\n"
89     };
90 }
91
92 macro_rules! RENAMES_SECTION_TEMPLATE {
93     () => {
94         r#"
95 ### Past names
96
97 {names}
98 "#
99     };
100 }
101 macro_rules! RENAME_VALUE_TEMPLATE {
102     () => {
103         "* `{name}`\n"
104     };
105 }
106
107 const LINT_EMISSION_FUNCTIONS: [&[&str]; 8] = [
108     &["clippy_utils", "diagnostics", "span_lint"],
109     &["clippy_utils", "diagnostics", "span_lint_and_help"],
110     &["clippy_utils", "diagnostics", "span_lint_and_note"],
111     &["clippy_utils", "diagnostics", "span_lint_hir"],
112     &["clippy_utils", "diagnostics", "span_lint_and_sugg"],
113     &["clippy_utils", "diagnostics", "span_lint_and_then"],
114     &["clippy_utils", "diagnostics", "span_lint_hir_and_then"],
115     &["clippy_utils", "diagnostics", "span_lint_and_sugg_for_edges"],
116 ];
117 const SUGGESTION_DIAGNOSTIC_BUILDER_METHODS: [(&str, bool); 9] = [
118     ("span_suggestion", false),
119     ("span_suggestion_short", false),
120     ("span_suggestion_verbose", false),
121     ("span_suggestion_hidden", false),
122     ("tool_only_span_suggestion", false),
123     ("multipart_suggestion", true),
124     ("multipart_suggestions", true),
125     ("tool_only_multipart_suggestion", true),
126     ("span_suggestions", true),
127 ];
128 const SUGGESTION_FUNCTIONS: [&[&str]; 2] = [
129     &["clippy_utils", "diagnostics", "multispan_sugg"],
130     &["clippy_utils", "diagnostics", "multispan_sugg_with_applicability"],
131 ];
132 const DEPRECATED_LINT_TYPE: [&str; 3] = ["clippy_lints", "deprecated_lints", "ClippyDeprecatedLint"];
133
134 /// The index of the applicability name of `paths::APPLICABILITY_VALUES`
135 const APPLICABILITY_NAME_INDEX: usize = 2;
136 /// This applicability will be set for unresolved applicability values.
137 const APPLICABILITY_UNRESOLVED_STR: &str = "Unresolved";
138 /// The version that will be displayed if none has been defined
139 const VERSION_DEFAULT_STR: &str = "Unknown";
140
141 declare_clippy_lint! {
142     /// ### What it does
143     /// Collects metadata about clippy lints for the website.
144     ///
145     /// This lint will be used to report problems of syntax parsing. You should hopefully never
146     /// see this but never say never I guess ^^
147     ///
148     /// ### Why is this bad?
149     /// This is not a bad thing but definitely a hacky way to do it. See
150     /// issue [#4310](https://github.com/rust-lang/rust-clippy/issues/4310) for a discussion
151     /// about the implementation.
152     ///
153     /// ### Known problems
154     /// Hopefully none. It would be pretty uncool to have a problem here :)
155     ///
156     /// ### Example output
157     /// ```json,ignore
158     /// {
159     ///     "id": "internal_metadata_collector",
160     ///     "id_span": {
161     ///         "path": "clippy_lints/src/utils/internal_lints/metadata_collector.rs",
162     ///         "line": 1
163     ///     },
164     ///     "group": "clippy::internal",
165     ///     "docs": " ### What it does\nCollects metadata about clippy lints for the website. [...] "
166     /// }
167     /// ```
168     #[clippy::version = "1.56.0"]
169     pub INTERNAL_METADATA_COLLECTOR,
170     internal_warn,
171     "A busy bee collection metadata about lints"
172 }
173
174 impl_lint_pass!(MetadataCollector => [INTERNAL_METADATA_COLLECTOR]);
175
176 #[allow(clippy::module_name_repetitions)]
177 #[derive(Debug, Clone)]
178 pub struct MetadataCollector {
179     /// All collected lints
180     ///
181     /// We use a Heap here to have the lints added in alphabetic order in the export
182     lints: BinaryHeap<LintMetadata>,
183     applicability_info: FxHashMap<String, ApplicabilityInfo>,
184     config: Vec<ClippyConfiguration>,
185     clippy_project_root: PathBuf,
186 }
187
188 impl MetadataCollector {
189     pub fn new() -> Self {
190         Self {
191             lints: BinaryHeap::<LintMetadata>::default(),
192             applicability_info: FxHashMap::<String, ApplicabilityInfo>::default(),
193             config: collect_configs(),
194             clippy_project_root: clippy_dev::clippy_project_root(),
195         }
196     }
197
198     fn get_lint_configs(&self, lint_name: &str) -> Option<String> {
199         self.config
200             .iter()
201             .filter(|config| config.lints.iter().any(|lint| lint == lint_name))
202             .map(ToString::to_string)
203             .reduce(|acc, x| acc + &x)
204             .map(|configurations| format!(CONFIGURATION_SECTION_TEMPLATE!(), configurations = configurations))
205     }
206 }
207
208 impl Drop for MetadataCollector {
209     /// You might ask: How hacky is this?
210     /// My answer:     YES
211     fn drop(&mut self) {
212         // The metadata collector gets dropped twice, this makes sure that we only write
213         // when the list is full
214         if self.lints.is_empty() {
215             return;
216         }
217
218         let mut applicability_info = std::mem::take(&mut self.applicability_info);
219
220         // Mapping the final data
221         let mut lints = std::mem::take(&mut self.lints).into_sorted_vec();
222         for x in &mut lints {
223             x.applicability = Some(applicability_info.remove(&x.id).unwrap_or_default());
224             replace_produces(&x.id, &mut x.docs, &self.clippy_project_root);
225         }
226
227         collect_renames(&mut lints);
228
229         // Outputting
230         if Path::new(OUTPUT_FILE).exists() {
231             fs::remove_file(OUTPUT_FILE).unwrap();
232         }
233         let mut file = OpenOptions::new().write(true).create(true).open(OUTPUT_FILE).unwrap();
234         writeln!(file, "{}", serde_json::to_string_pretty(&lints).unwrap()).unwrap();
235     }
236 }
237
238 #[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)]
239 struct LintMetadata {
240     id: String,
241     id_span: SerializableSpan,
242     group: String,
243     level: String,
244     docs: String,
245     version: String,
246     /// This field is only used in the output and will only be
247     /// mapped shortly before the actual output.
248     applicability: Option<ApplicabilityInfo>,
249 }
250
251 impl LintMetadata {
252     fn new(
253         id: String,
254         id_span: SerializableSpan,
255         group: String,
256         level: &'static str,
257         version: String,
258         docs: String,
259     ) -> Self {
260         Self {
261             id,
262             id_span,
263             group,
264             level: level.to_string(),
265             version,
266             docs,
267             applicability: None,
268         }
269     }
270 }
271
272 fn replace_produces(lint_name: &str, docs: &mut String, clippy_project_root: &Path) {
273     let mut doc_lines = docs.lines().map(ToString::to_string).collect::<Vec<_>>();
274     let mut lines = doc_lines.iter_mut();
275
276     'outer: loop {
277         // Find the start of the example
278
279         // ```rust
280         loop {
281             match lines.next() {
282                 Some(line) if line.trim_start().starts_with("```rust") => {
283                     if line.contains("ignore") || line.contains("no_run") {
284                         // A {{produces}} marker may have been put on a ignored code block by mistake,
285                         // just seek to the end of the code block and continue checking.
286                         if lines.any(|line| line.trim_start().starts_with("```")) {
287                             continue;
288                         }
289
290                         panic!("lint `{}` has an unterminated code block", lint_name)
291                     }
292
293                     break;
294                 },
295                 Some(line) if line.trim_start() == "{{produces}}" => {
296                     panic!(
297                         "lint `{}` has marker {{{{produces}}}} with an ignored or missing code block",
298                         lint_name
299                     )
300                 },
301                 Some(line) => {
302                     let line = line.trim();
303                     // These are the two most common markers of the corrections section
304                     if line.eq_ignore_ascii_case("Use instead:") || line.eq_ignore_ascii_case("Could be written as:") {
305                         break 'outer;
306                     }
307                 },
308                 None => break 'outer,
309             }
310         }
311
312         // Collect the example
313         let mut example = Vec::new();
314         loop {
315             match lines.next() {
316                 Some(line) if line.trim_start() == "```" => break,
317                 Some(line) => example.push(line),
318                 None => panic!("lint `{}` has an unterminated code block", lint_name),
319             }
320         }
321
322         // Find the {{produces}} and attempt to generate the output
323         loop {
324             match lines.next() {
325                 Some(line) if line.is_empty() => {},
326                 Some(line) if line.trim() == "{{produces}}" => {
327                     let output = get_lint_output(lint_name, &example, clippy_project_root);
328                     line.replace_range(
329                         ..,
330                         &format!(
331                             "<details>\
332                             <summary>Produces</summary>\n\
333                             \n\
334                             ```text\n\
335                             {}\n\
336                             ```\n\
337                         </details>",
338                             output
339                         ),
340                     );
341
342                     break;
343                 },
344                 // No {{produces}}, we can move on to the next example
345                 Some(_) => break,
346                 None => break 'outer,
347             }
348         }
349     }
350
351     *docs = cleanup_docs(&doc_lines);
352 }
353
354 fn get_lint_output(lint_name: &str, example: &[&mut String], clippy_project_root: &Path) -> String {
355     let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("failed to create temp dir: {e}"));
356     let file = dir.path().join("lint_example.rs");
357
358     let mut source = String::new();
359     let unhidden = example
360         .iter()
361         .map(|line| line.trim_start().strip_prefix("# ").unwrap_or(line));
362
363     // Get any attributes
364     let mut lines = unhidden.peekable();
365     while let Some(line) = lines.peek() {
366         if line.starts_with("#!") {
367             source.push_str(line);
368             source.push('\n');
369             lines.next();
370         } else {
371             break;
372         }
373     }
374
375     let needs_main = !example.iter().any(|line| line.contains("fn main"));
376     if needs_main {
377         source.push_str("fn main() {\n");
378     }
379
380     for line in lines {
381         source.push_str(line);
382         source.push('\n');
383     }
384
385     if needs_main {
386         source.push_str("}\n");
387     }
388
389     if let Err(e) = fs::write(&file, &source) {
390         panic!("failed to write to `{}`: {e}", file.as_path().to_string_lossy());
391     }
392
393     let prefixed_name = format!("{}{lint_name}", CLIPPY_LINT_GROUP_PREFIX);
394
395     let mut cmd = Command::new("cargo");
396
397     cmd.current_dir(clippy_project_root)
398         .env("CARGO_INCREMENTAL", "0")
399         .env("CLIPPY_ARGS", "")
400         .env("CLIPPY_DISABLE_DOCS_LINKS", "1")
401         // We need to disable this to enable all lints
402         .env("ENABLE_METADATA_COLLECTION", "0")
403         .args(["run", "--bin", "clippy-driver"])
404         .args(["--target-dir", "./clippy_lints/target"])
405         .args(["--", "--error-format=json"])
406         .args(["--edition", "2021"])
407         .arg("-Cdebuginfo=0")
408         .args(["-A", "clippy::all"])
409         .args(["-W", &prefixed_name])
410         .args(["-L", "./target/debug"])
411         .args(["-Z", "no-codegen"]);
412
413     let output = cmd
414         .arg(file.as_path())
415         .output()
416         .unwrap_or_else(|e| panic!("failed to run `{:?}`: {e}", cmd));
417
418     let tmp_file_path = file.to_string_lossy();
419     let stderr = std::str::from_utf8(&output.stderr).unwrap();
420     let msgs = stderr
421         .lines()
422         .filter(|line| line.starts_with('{'))
423         .map(|line| serde_json::from_str(line).unwrap())
424         .collect::<Vec<serde_json::Value>>();
425
426     let mut rendered = String::new();
427     let iter = msgs
428         .iter()
429         .filter(|msg| matches!(&msg["code"]["code"], serde_json::Value::String(s) if s == &prefixed_name));
430
431     for message in iter {
432         let rendered_part = message["rendered"].as_str().expect("rendered field should exist");
433         rendered.push_str(rendered_part);
434     }
435
436     if rendered.is_empty() {
437         let rendered: Vec<&str> = msgs.iter().filter_map(|msg| msg["rendered"].as_str()).collect();
438         let non_json: Vec<&str> = stderr.lines().filter(|line| !line.starts_with('{')).collect();
439         panic!(
440             "did not find lint `{}` in output of example, got:\n{}\n{}",
441             lint_name,
442             non_json.join("\n"),
443             rendered.join("\n")
444         );
445     }
446
447     // The reader doesn't need to see `/tmp/.tmpfiy2Qd/lint_example.rs` :)
448     rendered.trim_end().replace(&*tmp_file_path, "lint_example.rs")
449 }
450
451 #[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)]
452 struct SerializableSpan {
453     path: String,
454     line: usize,
455 }
456
457 impl fmt::Display for SerializableSpan {
458     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459         write!(f, "{}:{}", self.path.rsplit('/').next().unwrap_or_default(), self.line)
460     }
461 }
462
463 impl SerializableSpan {
464     fn from_item(cx: &LateContext<'_>, item: &Item<'_>) -> Self {
465         Self::from_span(cx, item.ident.span)
466     }
467
468     fn from_span(cx: &LateContext<'_>, span: Span) -> Self {
469         let loc: Loc = cx.sess().source_map().lookup_char_pos(span.lo());
470
471         Self {
472             path: format!("{}", loc.file.name.prefer_remapped()),
473             line: loc.line,
474         }
475     }
476 }
477
478 #[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
479 struct ApplicabilityInfo {
480     /// Indicates if any of the lint emissions uses multiple spans. This is related to
481     /// [rustfix#141](https://github.com/rust-lang/rustfix/issues/141) as such suggestions can
482     /// currently not be applied automatically.
483     is_multi_part_suggestion: bool,
484     applicability: Option<usize>,
485 }
486
487 impl Serialize for ApplicabilityInfo {
488     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
489     where
490         S: Serializer,
491     {
492         let mut s = serializer.serialize_struct("ApplicabilityInfo", 2)?;
493         s.serialize_field("is_multi_part_suggestion", &self.is_multi_part_suggestion)?;
494         if let Some(index) = self.applicability {
495             s.serialize_field(
496                 "applicability",
497                 &paths::APPLICABILITY_VALUES[index][APPLICABILITY_NAME_INDEX],
498             )?;
499         } else {
500             s.serialize_field("applicability", APPLICABILITY_UNRESOLVED_STR)?;
501         }
502         s.end()
503     }
504 }
505
506 // ==================================================================
507 // Configuration
508 // ==================================================================
509 #[derive(Debug, Clone, Default)]
510 pub struct ClippyConfiguration {
511     name: String,
512     config_type: &'static str,
513     default: String,
514     lints: Vec<String>,
515     doc: String,
516     #[allow(dead_code)]
517     deprecation_reason: Option<&'static str>,
518 }
519
520 impl ClippyConfiguration {
521     pub fn new(
522         name: &'static str,
523         config_type: &'static str,
524         default: String,
525         doc_comment: &'static str,
526         deprecation_reason: Option<&'static str>,
527     ) -> Self {
528         let (lints, doc) = parse_config_field_doc(doc_comment)
529             .unwrap_or_else(|| (vec![], "[ERROR] MALFORMED DOC COMMENT".to_string()));
530
531         Self {
532             name: to_kebab(name),
533             lints,
534             doc,
535             config_type,
536             default,
537             deprecation_reason,
538         }
539     }
540 }
541
542 fn collect_configs() -> Vec<ClippyConfiguration> {
543     crate::utils::conf::metadata::get_configuration_metadata()
544 }
545
546 /// This parses the field documentation of the config struct.
547 ///
548 /// ```rust, ignore
549 /// parse_config_field_doc(cx, "Lint: LINT_NAME_1, LINT_NAME_2. Papa penguin, papa penguin")
550 /// ```
551 ///
552 /// Would yield:
553 /// ```rust, ignore
554 /// Some(["lint_name_1", "lint_name_2"], "Papa penguin, papa penguin")
555 /// ```
556 fn parse_config_field_doc(doc_comment: &str) -> Option<(Vec<String>, String)> {
557     const DOC_START: &str = " Lint: ";
558     if_chain! {
559         if doc_comment.starts_with(DOC_START);
560         if let Some(split_pos) = doc_comment.find('.');
561         then {
562             let mut doc_comment = doc_comment.to_string();
563             let mut documentation = doc_comment.split_off(split_pos);
564
565             // Extract lints
566             doc_comment.make_ascii_lowercase();
567             let lints: Vec<String> = doc_comment.split_off(DOC_START.len()).split(", ").map(str::to_string).collect();
568
569             // Format documentation correctly
570             // split off leading `.` from lint name list and indent for correct formatting
571             documentation = documentation.trim_start_matches('.').trim().replace("\n ", "\n    ");
572
573             Some((lints, documentation))
574         } else {
575             None
576         }
577     }
578 }
579
580 /// Transforms a given `snake_case_string` to a tasty `kebab-case-string`
581 fn to_kebab(config_name: &str) -> String {
582     config_name.replace('_', "-")
583 }
584
585 impl fmt::Display for ClippyConfiguration {
586     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
587         write!(
588             f,
589             CONFIGURATION_VALUE_TEMPLATE!(),
590             name = self.name,
591             ty = self.config_type,
592             doc = self.doc,
593             default = self.default
594         )
595     }
596 }
597
598 // ==================================================================
599 // Lint pass
600 // ==================================================================
601 impl<'hir> LateLintPass<'hir> for MetadataCollector {
602     /// Collecting lint declarations like:
603     /// ```rust, ignore
604     /// declare_clippy_lint! {
605     ///     /// ### What it does
606     ///     /// Something IDK.
607     ///     pub SOME_LINT,
608     ///     internal,
609     ///     "Who am I?"
610     /// }
611     /// ```
612     fn check_item(&mut self, cx: &LateContext<'hir>, item: &'hir Item<'_>) {
613         if let ItemKind::Static(ty, Mutability::Not, _) = item.kind {
614             // Normal lint
615             if_chain! {
616                 // item validation
617                 if is_lint_ref_type(cx, ty);
618                 // blacklist check
619                 let lint_name = sym_to_string(item.ident.name).to_ascii_lowercase();
620                 if !BLACK_LISTED_LINTS.contains(&lint_name.as_str());
621                 // metadata extraction
622                 if let Some((group, level)) = get_lint_group_and_level_or_lint(cx, &lint_name, item);
623                 if let Some(mut raw_docs) = extract_attr_docs_or_lint(cx, item);
624                 then {
625                     if let Some(configuration_section) = self.get_lint_configs(&lint_name) {
626                         raw_docs.push_str(&configuration_section);
627                     }
628                     let version = get_lint_version(cx, item);
629
630                     self.lints.push(LintMetadata::new(
631                         lint_name,
632                         SerializableSpan::from_item(cx, item),
633                         group,
634                         level,
635                         version,
636                         raw_docs,
637                     ));
638                 }
639             }
640
641             if_chain! {
642                 if is_deprecated_lint(cx, ty);
643                 // blacklist check
644                 let lint_name = sym_to_string(item.ident.name).to_ascii_lowercase();
645                 if !BLACK_LISTED_LINTS.contains(&lint_name.as_str());
646                 // Metadata the little we can get from a deprecated lint
647                 if let Some(raw_docs) = extract_attr_docs_or_lint(cx, item);
648                 then {
649                     let version = get_lint_version(cx, item);
650
651                     self.lints.push(LintMetadata::new(
652                         lint_name,
653                         SerializableSpan::from_item(cx, item),
654                         DEPRECATED_LINT_GROUP_STR.to_string(),
655                         DEPRECATED_LINT_LEVEL,
656                         version,
657                         raw_docs,
658                     ));
659                 }
660             }
661         }
662     }
663
664     /// Collecting constant applicability from the actual lint emissions
665     ///
666     /// Example:
667     /// ```rust, ignore
668     /// span_lint_and_sugg(
669     ///     cx,
670     ///     SOME_LINT,
671     ///     item.span,
672     ///     "Le lint message",
673     ///     "Here comes help:",
674     ///     "#![allow(clippy::all)]",
675     ///     Applicability::MachineApplicable, // <-- Extracts this constant value
676     /// );
677     /// ```
678     fn check_expr(&mut self, cx: &LateContext<'hir>, expr: &'hir hir::Expr<'_>) {
679         if let Some(args) = match_lint_emission(cx, expr) {
680             let emission_info = extract_emission_info(cx, args);
681             if emission_info.is_empty() {
682                 // See:
683                 // - src/misc.rs:734:9
684                 // - src/methods/mod.rs:3545:13
685                 // - src/methods/mod.rs:3496:13
686                 // We are basically unable to resolve the lint name itself.
687                 return;
688             }
689
690             for (lint_name, applicability, is_multi_part) in emission_info {
691                 let app_info = self.applicability_info.entry(lint_name).or_default();
692                 app_info.applicability = applicability;
693                 app_info.is_multi_part_suggestion = is_multi_part;
694             }
695         }
696     }
697 }
698
699 // ==================================================================
700 // Lint definition extraction
701 // ==================================================================
702 fn sym_to_string(sym: Symbol) -> String {
703     sym.as_str().to_string()
704 }
705
706 fn extract_attr_docs_or_lint(cx: &LateContext<'_>, item: &Item<'_>) -> Option<String> {
707     extract_attr_docs(cx, item).or_else(|| {
708         lint_collection_error_item(cx, item, "could not collect the lint documentation");
709         None
710     })
711 }
712
713 /// This function collects all documentation that has been added to an item using
714 /// `#[doc = r""]` attributes. Several attributes are aggravated using line breaks
715 ///
716 /// ```ignore
717 /// #[doc = r"Hello world!"]
718 /// #[doc = r"=^.^="]
719 /// struct SomeItem {}
720 /// ```
721 ///
722 /// Would result in `Hello world!\n=^.^=\n`
723 fn extract_attr_docs(cx: &LateContext<'_>, item: &Item<'_>) -> Option<String> {
724     let attrs = cx.tcx.hir().attrs(item.hir_id());
725     let mut lines = attrs.iter().filter_map(ast::Attribute::doc_str);
726
727     if let Some(line) = lines.next() {
728         let raw_docs = lines.fold(String::from(line.as_str()) + "\n", |s, line| s + line.as_str() + "\n");
729         return Some(raw_docs);
730     }
731
732     None
733 }
734
735 /// This function may modify the doc comment to ensure that the string can be displayed using a
736 /// markdown viewer in Clippy's lint list. The following modifications could be applied:
737 /// * Removal of leading space after a new line. (Important to display tables)
738 /// * Ensures that code blocks only contain language information
739 fn cleanup_docs(docs_collection: &Vec<String>) -> String {
740     let mut in_code_block = false;
741     let mut is_code_block_rust = false;
742
743     let mut docs = String::new();
744     for line in docs_collection {
745         // Rustdoc hides code lines starting with `# ` and this removes them from Clippy's lint list :)
746         if is_code_block_rust && line.trim_start().starts_with("# ") {
747             continue;
748         }
749
750         // The line should be represented in the lint list, even if it's just an empty line
751         docs.push('\n');
752         if let Some(info) = line.trim_start().strip_prefix("```") {
753             in_code_block = !in_code_block;
754             is_code_block_rust = false;
755             if in_code_block {
756                 let lang = info
757                     .trim()
758                     .split(',')
759                     // remove rustdoc directives
760                     .find(|&s| !matches!(s, "" | "ignore" | "no_run" | "should_panic"))
761                     // if no language is present, fill in "rust"
762                     .unwrap_or("rust");
763                 docs.push_str("```");
764                 docs.push_str(lang);
765
766                 is_code_block_rust = lang == "rust";
767                 continue;
768             }
769         }
770         // This removes the leading space that the macro translation introduces
771         if let Some(stripped_doc) = line.strip_prefix(' ') {
772             docs.push_str(stripped_doc);
773         } else if !line.is_empty() {
774             docs.push_str(line);
775         }
776     }
777
778     docs
779 }
780
781 fn get_lint_version(cx: &LateContext<'_>, item: &Item<'_>) -> String {
782     extract_clippy_version_value(cx, item).map_or_else(
783         || VERSION_DEFAULT_STR.to_string(),
784         |version| version.as_str().to_string(),
785     )
786 }
787
788 fn get_lint_group_and_level_or_lint(
789     cx: &LateContext<'_>,
790     lint_name: &str,
791     item: &Item<'_>,
792 ) -> Option<(String, &'static str)> {
793     let result = cx.lint_store.check_lint_name(
794         lint_name,
795         Some(sym::clippy),
796         &[Ident::with_dummy_span(sym::clippy)].into_iter().collect(),
797     );
798     if let CheckLintNameResult::Tool(Ok(lint_lst)) = result {
799         if let Some(group) = get_lint_group(cx, lint_lst[0]) {
800             if EXCLUDED_LINT_GROUPS.contains(&group.as_str()) {
801                 return None;
802             }
803
804             if let Some(level) = get_lint_level_from_group(&group) {
805                 Some((group, level))
806             } else {
807                 lint_collection_error_item(
808                     cx,
809                     item,
810                     &format!("Unable to determine lint level for found group `{}`", group),
811                 );
812                 None
813             }
814         } else {
815             lint_collection_error_item(cx, item, "Unable to determine lint group");
816             None
817         }
818     } else {
819         lint_collection_error_item(cx, item, "Unable to find lint in lint_store");
820         None
821     }
822 }
823
824 fn get_lint_group(cx: &LateContext<'_>, lint_id: LintId) -> Option<String> {
825     for (group_name, lints, _) in cx.lint_store.get_lint_groups() {
826         if IGNORED_LINT_GROUPS.contains(&group_name) {
827             continue;
828         }
829
830         if lints.iter().any(|group_lint| *group_lint == lint_id) {
831             let group = group_name.strip_prefix(CLIPPY_LINT_GROUP_PREFIX).unwrap_or(group_name);
832             return Some((*group).to_string());
833         }
834     }
835
836     None
837 }
838
839 fn get_lint_level_from_group(lint_group: &str) -> Option<&'static str> {
840     DEFAULT_LINT_LEVELS
841         .iter()
842         .find_map(|(group_name, group_level)| (*group_name == lint_group).then(|| *group_level))
843 }
844
845 fn is_deprecated_lint(cx: &LateContext<'_>, ty: &hir::Ty<'_>) -> bool {
846     if let hir::TyKind::Path(ref path) = ty.kind {
847         if let hir::def::Res::Def(DefKind::Struct, def_id) = cx.qpath_res(path, ty.hir_id) {
848             return match_def_path(cx, def_id, &DEPRECATED_LINT_TYPE);
849         }
850     }
851
852     false
853 }
854
855 fn collect_renames(lints: &mut Vec<LintMetadata>) {
856     for lint in lints {
857         let mut collected = String::new();
858         let mut names = vec![lint.id.clone()];
859
860         loop {
861             if let Some(lint_name) = names.pop() {
862                 for (k, v) in RENAMED_LINTS {
863                     if_chain! {
864                         if let Some(name) = v.strip_prefix(CLIPPY_LINT_GROUP_PREFIX);
865                         if name == lint_name;
866                         if let Some(past_name) = k.strip_prefix(CLIPPY_LINT_GROUP_PREFIX);
867                         then {
868                             write!(collected, RENAME_VALUE_TEMPLATE!(), name = past_name).unwrap();
869                             names.push(past_name.to_string());
870                         }
871                     }
872                 }
873
874                 continue;
875             }
876
877             break;
878         }
879
880         if !collected.is_empty() {
881             write!(&mut lint.docs, RENAMES_SECTION_TEMPLATE!(), names = collected).unwrap();
882         }
883     }
884 }
885
886 // ==================================================================
887 // Lint emission
888 // ==================================================================
889 fn lint_collection_error_item(cx: &LateContext<'_>, item: &Item<'_>, message: &str) {
890     span_lint(
891         cx,
892         INTERNAL_METADATA_COLLECTOR,
893         item.ident.span,
894         &format!("metadata collection error for `{}`: {}", item.ident.name, message),
895     );
896 }
897
898 // ==================================================================
899 // Applicability
900 // ==================================================================
901 /// This function checks if a given expression is equal to a simple lint emission function call.
902 /// It will return the function arguments if the emission matched any function.
903 fn match_lint_emission<'hir>(cx: &LateContext<'hir>, expr: &'hir hir::Expr<'_>) -> Option<&'hir [hir::Expr<'hir>]> {
904     LINT_EMISSION_FUNCTIONS
905         .iter()
906         .find_map(|emission_fn| match_function_call(cx, expr, emission_fn))
907 }
908
909 fn take_higher_applicability(a: Option<usize>, b: Option<usize>) -> Option<usize> {
910     a.map_or(b, |a| a.max(b.unwrap_or_default()).into())
911 }
912
913 fn extract_emission_info<'hir>(
914     cx: &LateContext<'hir>,
915     args: &'hir [hir::Expr<'hir>],
916 ) -> Vec<(String, Option<usize>, bool)> {
917     let mut lints = Vec::new();
918     let mut applicability = None;
919     let mut multi_part = false;
920
921     for arg in args {
922         let (arg_ty, _) = walk_ptrs_ty_depth(cx.typeck_results().expr_ty(arg));
923
924         if match_type(cx, arg_ty, &paths::LINT) {
925             // If we found the lint arg, extract the lint name
926             let mut resolved_lints = resolve_lints(cx, arg);
927             lints.append(&mut resolved_lints);
928         } else if match_type(cx, arg_ty, &paths::APPLICABILITY) {
929             applicability = resolve_applicability(cx, arg);
930         } else if arg_ty.is_closure() {
931             multi_part |= check_is_multi_part(cx, arg);
932             applicability = applicability.or_else(|| resolve_applicability(cx, arg));
933         }
934     }
935
936     lints
937         .into_iter()
938         .map(|lint_name| (lint_name, applicability, multi_part))
939         .collect()
940 }
941
942 /// Resolves the possible lints that this expression could reference
943 fn resolve_lints<'hir>(cx: &LateContext<'hir>, expr: &'hir hir::Expr<'hir>) -> Vec<String> {
944     let mut resolver = LintResolver::new(cx);
945     resolver.visit_expr(expr);
946     resolver.lints
947 }
948
949 /// This function tries to resolve the linked applicability to the given expression.
950 fn resolve_applicability<'hir>(cx: &LateContext<'hir>, expr: &'hir hir::Expr<'hir>) -> Option<usize> {
951     let mut resolver = ApplicabilityResolver::new(cx);
952     resolver.visit_expr(expr);
953     resolver.complete()
954 }
955
956 fn check_is_multi_part<'hir>(cx: &LateContext<'hir>, closure_expr: &'hir hir::Expr<'hir>) -> bool {
957     if let ExprKind::Closure { body, .. } = closure_expr.kind {
958         let mut scanner = IsMultiSpanScanner::new(cx);
959         intravisit::walk_body(&mut scanner, cx.tcx.hir().body(body));
960         return scanner.is_multi_part();
961     } else if let Some(local) = get_parent_local(cx, closure_expr) {
962         if let Some(local_init) = local.init {
963             return check_is_multi_part(cx, local_init);
964         }
965     }
966
967     false
968 }
969
970 struct LintResolver<'a, 'hir> {
971     cx: &'a LateContext<'hir>,
972     lints: Vec<String>,
973 }
974
975 impl<'a, 'hir> LintResolver<'a, 'hir> {
976     fn new(cx: &'a LateContext<'hir>) -> Self {
977         Self {
978             cx,
979             lints: Vec::<String>::default(),
980         }
981     }
982 }
983
984 impl<'a, 'hir> intravisit::Visitor<'hir> for LintResolver<'a, 'hir> {
985     type NestedFilter = nested_filter::All;
986
987     fn nested_visit_map(&mut self) -> Self::Map {
988         self.cx.tcx.hir()
989     }
990
991     fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) {
992         if_chain! {
993             if let ExprKind::Path(qpath) = &expr.kind;
994             if let QPath::Resolved(_, path) = qpath;
995
996             let (expr_ty, _) = walk_ptrs_ty_depth(self.cx.typeck_results().expr_ty(expr));
997             if match_type(self.cx, expr_ty, &paths::LINT);
998             then {
999                 if let hir::def::Res::Def(DefKind::Static(..), _) = path.res {
1000                     let lint_name = last_path_segment(qpath).ident.name;
1001                     self.lints.push(sym_to_string(lint_name).to_ascii_lowercase());
1002                 } else if let Some(local) = get_parent_local(self.cx, expr) {
1003                     if let Some(local_init) = local.init {
1004                         intravisit::walk_expr(self, local_init);
1005                     }
1006                 }
1007             }
1008         }
1009
1010         intravisit::walk_expr(self, expr);
1011     }
1012 }
1013
1014 /// This visitor finds the highest applicability value in the visited expressions
1015 struct ApplicabilityResolver<'a, 'hir> {
1016     cx: &'a LateContext<'hir>,
1017     /// This is the index of hightest `Applicability` for `paths::APPLICABILITY_VALUES`
1018     applicability_index: Option<usize>,
1019 }
1020
1021 impl<'a, 'hir> ApplicabilityResolver<'a, 'hir> {
1022     fn new(cx: &'a LateContext<'hir>) -> Self {
1023         Self {
1024             cx,
1025             applicability_index: None,
1026         }
1027     }
1028
1029     fn add_new_index(&mut self, new_index: usize) {
1030         self.applicability_index = take_higher_applicability(self.applicability_index, Some(new_index));
1031     }
1032
1033     fn complete(self) -> Option<usize> {
1034         self.applicability_index
1035     }
1036 }
1037
1038 impl<'a, 'hir> intravisit::Visitor<'hir> for ApplicabilityResolver<'a, 'hir> {
1039     type NestedFilter = nested_filter::All;
1040
1041     fn nested_visit_map(&mut self) -> Self::Map {
1042         self.cx.tcx.hir()
1043     }
1044
1045     fn visit_path(&mut self, path: &'hir hir::Path<'hir>, _id: hir::HirId) {
1046         for (index, enum_value) in paths::APPLICABILITY_VALUES.iter().enumerate() {
1047             if match_path(path, enum_value) {
1048                 self.add_new_index(index);
1049                 return;
1050             }
1051         }
1052     }
1053
1054     fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) {
1055         let (expr_ty, _) = walk_ptrs_ty_depth(self.cx.typeck_results().expr_ty(expr));
1056
1057         if_chain! {
1058             if match_type(self.cx, expr_ty, &paths::APPLICABILITY);
1059             if let Some(local) = get_parent_local(self.cx, expr);
1060             if let Some(local_init) = local.init;
1061             then {
1062                 intravisit::walk_expr(self, local_init);
1063             }
1064         };
1065
1066         intravisit::walk_expr(self, expr);
1067     }
1068 }
1069
1070 /// This returns the parent local node if the expression is a reference one
1071 fn get_parent_local<'hir>(cx: &LateContext<'hir>, expr: &'hir hir::Expr<'hir>) -> Option<&'hir hir::Local<'hir>> {
1072     if let ExprKind::Path(QPath::Resolved(_, path)) = expr.kind {
1073         if let hir::def::Res::Local(local_hir) = path.res {
1074             return get_parent_local_hir_id(cx, local_hir);
1075         }
1076     }
1077
1078     None
1079 }
1080
1081 fn get_parent_local_hir_id<'hir>(cx: &LateContext<'hir>, hir_id: hir::HirId) -> Option<&'hir hir::Local<'hir>> {
1082     let map = cx.tcx.hir();
1083
1084     match map.find(map.get_parent_node(hir_id)) {
1085         Some(hir::Node::Local(local)) => Some(local),
1086         Some(hir::Node::Pat(pattern)) => get_parent_local_hir_id(cx, pattern.hir_id),
1087         _ => None,
1088     }
1089 }
1090
1091 /// This visitor finds the highest applicability value in the visited expressions
1092 struct IsMultiSpanScanner<'a, 'hir> {
1093     cx: &'a LateContext<'hir>,
1094     suggestion_count: usize,
1095 }
1096
1097 impl<'a, 'hir> IsMultiSpanScanner<'a, 'hir> {
1098     fn new(cx: &'a LateContext<'hir>) -> Self {
1099         Self {
1100             cx,
1101             suggestion_count: 0,
1102         }
1103     }
1104
1105     /// Add a new single expression suggestion to the counter
1106     fn add_single_span_suggestion(&mut self) {
1107         self.suggestion_count += 1;
1108     }
1109
1110     /// Signals that a suggestion with possible multiple spans was found
1111     fn add_multi_part_suggestion(&mut self) {
1112         self.suggestion_count += 2;
1113     }
1114
1115     /// Checks if the suggestions include multiple spans
1116     fn is_multi_part(&self) -> bool {
1117         self.suggestion_count > 1
1118     }
1119 }
1120
1121 impl<'a, 'hir> intravisit::Visitor<'hir> for IsMultiSpanScanner<'a, 'hir> {
1122     type NestedFilter = nested_filter::All;
1123
1124     fn nested_visit_map(&mut self) -> Self::Map {
1125         self.cx.tcx.hir()
1126     }
1127
1128     fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) {
1129         // Early return if the lint is already multi span
1130         if self.is_multi_part() {
1131             return;
1132         }
1133
1134         match &expr.kind {
1135             ExprKind::Call(fn_expr, _args) => {
1136                 let found_function = SUGGESTION_FUNCTIONS
1137                     .iter()
1138                     .any(|func_path| match_function_call(self.cx, fn_expr, func_path).is_some());
1139                 if found_function {
1140                     // These functions are all multi part suggestions
1141                     self.add_single_span_suggestion();
1142                 }
1143             },
1144             ExprKind::MethodCall(path, arg, _arg_span) => {
1145                 let (self_ty, _) = walk_ptrs_ty_depth(self.cx.typeck_results().expr_ty(&arg[0]));
1146                 if match_type(self.cx, self_ty, &paths::DIAGNOSTIC_BUILDER) {
1147                     let called_method = path.ident.name.as_str().to_string();
1148                     for (method_name, is_multi_part) in &SUGGESTION_DIAGNOSTIC_BUILDER_METHODS {
1149                         if *method_name == called_method {
1150                             if *is_multi_part {
1151                                 self.add_multi_part_suggestion();
1152                             } else {
1153                                 self.add_single_span_suggestion();
1154                             }
1155                             break;
1156                         }
1157                     }
1158                 }
1159             },
1160             _ => {},
1161         }
1162
1163         intravisit::walk_expr(self, expr);
1164     }
1165 }