]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
1 | #![cfg_attr(feature = "deny-warnings", deny(warnings))] |
2 | #![feature(once_cell)] | |
3 | ||
4 | use itertools::Itertools; | |
5 | use regex::Regex; | |
6 | use std::collections::HashMap; | |
7 | use std::ffi::OsStr; | |
8 | use std::fs; | |
9 | use std::lazy::SyncLazy; | |
10 | use std::path::{Path, PathBuf}; | |
11 | use walkdir::WalkDir; | |
12 | ||
13 | pub mod bless; | |
14 | pub mod fmt; | |
15 | pub mod new_lint; | |
16 | pub mod ra_setup; | |
17 | pub mod serve; | |
18 | pub mod stderr_length_check; | |
19 | pub mod update_lints; | |
20 | ||
21 | static DEC_CLIPPY_LINT_RE: SyncLazy<Regex> = SyncLazy::new(|| { | |
22 | Regex::new( | |
23 | r#"(?x) | |
24 | declare_clippy_lint!\s*[\{(] | |
25 | (?:\s+///.*)* | |
26 | \s+pub\s+(?P<name>[A-Z_][A-Z_0-9]*)\s*,\s* | |
27 | (?P<cat>[a-z_]+)\s*,\s* | |
28 | "(?P<desc>(?:[^"\\]+|\\(?s).(?-s))*)"\s*[})] | |
29 | "#, | |
30 | ) | |
31 | .unwrap() | |
32 | }); | |
33 | ||
34 | static DEC_DEPRECATED_LINT_RE: SyncLazy<Regex> = SyncLazy::new(|| { | |
35 | Regex::new( | |
36 | r#"(?x) | |
37 | declare_deprecated_lint!\s*[{(]\s* | |
38 | (?:\s+///.*)* | |
39 | \s+pub\s+(?P<name>[A-Z_][A-Z_0-9]*)\s*,\s* | |
40 | "(?P<desc>(?:[^"\\]+|\\(?s).(?-s))*)"\s*[})] | |
41 | "#, | |
42 | ) | |
43 | .unwrap() | |
44 | }); | |
45 | static NL_ESCAPE_RE: SyncLazy<Regex> = SyncLazy::new(|| Regex::new(r#"\\\n\s*"#).unwrap()); | |
46 | ||
47 | pub static DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html"; | |
48 | ||
49 | /// Lint data parsed from the Clippy source code. | |
50 | #[derive(Clone, PartialEq, Debug)] | |
51 | pub struct Lint { | |
52 | pub name: String, | |
53 | pub group: String, | |
54 | pub desc: String, | |
55 | pub deprecation: Option<String>, | |
56 | pub module: String, | |
57 | } | |
58 | ||
59 | impl Lint { | |
60 | #[must_use] | |
61 | pub fn new(name: &str, group: &str, desc: &str, deprecation: Option<&str>, module: &str) -> Self { | |
62 | Self { | |
63 | name: name.to_lowercase(), | |
64 | group: group.to_string(), | |
65 | desc: NL_ESCAPE_RE.replace(&desc.replace("\\\"", "\""), "").to_string(), | |
66 | deprecation: deprecation.map(ToString::to_string), | |
67 | module: module.to_string(), | |
68 | } | |
69 | } | |
70 | ||
71 | /// Returns all non-deprecated lints and non-internal lints | |
72 | #[must_use] | |
73 | pub fn usable_lints(lints: &[Self]) -> Vec<Self> { | |
74 | lints | |
75 | .iter() | |
76 | .filter(|l| l.deprecation.is_none() && !l.group.starts_with("internal")) | |
77 | .cloned() | |
78 | .collect() | |
79 | } | |
80 | ||
81 | /// Returns all internal lints (not `internal_warn` lints) | |
82 | #[must_use] | |
83 | pub fn internal_lints(lints: &[Self]) -> Vec<Self> { | |
84 | lints.iter().filter(|l| l.group == "internal").cloned().collect() | |
85 | } | |
86 | ||
87 | /// Returns all deprecated lints | |
88 | #[must_use] | |
89 | pub fn deprecated_lints(lints: &[Self]) -> Vec<Self> { | |
90 | lints.iter().filter(|l| l.deprecation.is_some()).cloned().collect() | |
91 | } | |
92 | ||
93 | /// Returns the lints in a `HashMap`, grouped by the different lint groups | |
94 | #[must_use] | |
95 | pub fn by_lint_group(lints: impl Iterator<Item = Self>) -> HashMap<String, Vec<Self>> { | |
96 | lints.map(|lint| (lint.group.to_string(), lint)).into_group_map() | |
97 | } | |
98 | } | |
99 | ||
100 | /// Generates the Vec items for `register_lint_group` calls in `clippy_lints/src/lib.rs`. | |
101 | #[must_use] | |
102 | pub fn gen_lint_group_list<'a>(lints: impl Iterator<Item = &'a Lint>) -> Vec<String> { | |
103 | lints | |
104 | .map(|l| format!(" LintId::of(&{}::{}),", l.module, l.name.to_uppercase())) | |
105 | .sorted() | |
106 | .collect::<Vec<String>>() | |
107 | } | |
108 | ||
109 | /// Generates the `pub mod module_name` list in `clippy_lints/src/lib.rs`. | |
110 | #[must_use] | |
111 | pub fn gen_modules_list<'a>(lints: impl Iterator<Item = &'a Lint>) -> Vec<String> { | |
112 | lints | |
113 | .map(|l| &l.module) | |
114 | .unique() | |
115 | .map(|module| format!("mod {};", module)) | |
116 | .sorted() | |
117 | .collect::<Vec<String>>() | |
118 | } | |
119 | ||
120 | /// Generates the list of lint links at the bottom of the README | |
121 | #[must_use] | |
122 | pub fn gen_changelog_lint_list<'a>(lints: impl Iterator<Item = &'a Lint>) -> Vec<String> { | |
123 | lints | |
124 | .sorted_by_key(|l| &l.name) | |
125 | .map(|l| format!("[`{}`]: {}#{}", l.name, DOCS_LINK, l.name)) | |
126 | .collect() | |
127 | } | |
128 | ||
129 | /// Generates the `register_removed` code in `./clippy_lints/src/lib.rs`. | |
130 | #[must_use] | |
131 | pub fn gen_deprecated<'a>(lints: impl Iterator<Item = &'a Lint>) -> Vec<String> { | |
132 | lints | |
133 | .flat_map(|l| { | |
134 | l.deprecation | |
135 | .clone() | |
136 | .map(|depr_text| { | |
137 | vec![ | |
138 | " store.register_removed(".to_string(), | |
139 | format!(" \"clippy::{}\",", l.name), | |
140 | format!(" \"{}\",", depr_text), | |
141 | " );".to_string(), | |
142 | ] | |
143 | }) | |
144 | .expect("only deprecated lints should be passed") | |
145 | }) | |
146 | .collect::<Vec<String>>() | |
147 | } | |
148 | ||
149 | #[must_use] | |
150 | pub fn gen_register_lint_list<'a>( | |
151 | internal_lints: impl Iterator<Item = &'a Lint>, | |
152 | usable_lints: impl Iterator<Item = &'a Lint>, | |
153 | ) -> Vec<String> { | |
154 | let header = " store.register_lints(&[".to_string(); | |
155 | let footer = " ]);".to_string(); | |
156 | let internal_lints = internal_lints | |
157 | .sorted_by_key(|l| format!(" &{}::{},", l.module, l.name.to_uppercase())) | |
158 | .map(|l| { | |
159 | format!( | |
160 | " #[cfg(feature = \"internal-lints\")]\n &{}::{},", | |
161 | l.module, | |
162 | l.name.to_uppercase() | |
163 | ) | |
164 | }); | |
165 | let other_lints = usable_lints | |
166 | .sorted_by_key(|l| format!(" &{}::{},", l.module, l.name.to_uppercase())) | |
167 | .map(|l| format!(" &{}::{},", l.module, l.name.to_uppercase())) | |
168 | .sorted(); | |
169 | let mut lint_list = vec![header]; | |
170 | lint_list.extend(internal_lints); | |
171 | lint_list.extend(other_lints); | |
172 | lint_list.push(footer); | |
173 | lint_list | |
174 | } | |
175 | ||
176 | /// Gathers all files in `src/clippy_lints` and gathers all lints inside | |
177 | pub fn gather_all() -> impl Iterator<Item = Lint> { | |
178 | lint_files().flat_map(|f| gather_from_file(&f)) | |
179 | } | |
180 | ||
181 | fn gather_from_file(dir_entry: &walkdir::DirEntry) -> impl Iterator<Item = Lint> { | |
182 | let content = fs::read_to_string(dir_entry.path()).unwrap(); | |
183 | let path = dir_entry.path(); | |
184 | let filename = path.file_stem().unwrap(); | |
185 | let path_buf = path.with_file_name(filename); | |
186 | let mut rel_path = path_buf | |
187 | .strip_prefix(clippy_project_root().join("clippy_lints/src")) | |
188 | .expect("only files in `clippy_lints/src` should be looked at"); | |
189 | // If the lints are stored in mod.rs, we get the module name from | |
190 | // the containing directory: | |
191 | if filename == "mod" { | |
192 | rel_path = rel_path.parent().unwrap(); | |
193 | } | |
194 | ||
195 | let module = rel_path | |
196 | .components() | |
197 | .map(|c| c.as_os_str().to_str().unwrap()) | |
198 | .collect::<Vec<_>>() | |
199 | .join("::"); | |
200 | ||
201 | parse_contents(&content, &module) | |
202 | } | |
203 | ||
204 | fn parse_contents(content: &str, module: &str) -> impl Iterator<Item = Lint> { | |
205 | let lints = DEC_CLIPPY_LINT_RE | |
206 | .captures_iter(content) | |
207 | .map(|m| Lint::new(&m["name"], &m["cat"], &m["desc"], None, module)); | |
208 | let deprecated = DEC_DEPRECATED_LINT_RE | |
209 | .captures_iter(content) | |
210 | .map(|m| Lint::new(&m["name"], "Deprecated", &m["desc"], Some(&m["desc"]), module)); | |
211 | // Removing the `.collect::<Vec<Lint>>().into_iter()` causes some lifetime issues due to the map | |
212 | lints.chain(deprecated).collect::<Vec<Lint>>().into_iter() | |
213 | } | |
214 | ||
215 | /// Collects all .rs files in the `clippy_lints/src` directory | |
216 | fn lint_files() -> impl Iterator<Item = walkdir::DirEntry> { | |
217 | // We use `WalkDir` instead of `fs::read_dir` here in order to recurse into subdirectories. | |
218 | // Otherwise we would not collect all the lints, for example in `clippy_lints/src/methods/`. | |
219 | let path = clippy_project_root().join("clippy_lints/src"); | |
220 | WalkDir::new(path) | |
221 | .into_iter() | |
222 | .filter_map(Result::ok) | |
223 | .filter(|f| f.path().extension() == Some(OsStr::new("rs"))) | |
224 | } | |
225 | ||
226 | /// Whether a file has had its text changed or not | |
227 | #[derive(PartialEq, Debug)] | |
228 | pub struct FileChange { | |
229 | pub changed: bool, | |
230 | pub new_lines: String, | |
231 | } | |
232 | ||
233 | /// Replaces a region in a file delimited by two lines matching regexes. | |
234 | /// | |
235 | /// `path` is the relative path to the file on which you want to perform the replacement. | |
236 | /// | |
237 | /// See `replace_region_in_text` for documentation of the other options. | |
238 | /// | |
239 | /// # Panics | |
240 | /// | |
241 | /// Panics if the path could not read or then written | |
242 | pub fn replace_region_in_file<F>( | |
243 | path: &Path, | |
244 | start: &str, | |
245 | end: &str, | |
246 | replace_start: bool, | |
247 | write_back: bool, | |
248 | replacements: F, | |
249 | ) -> FileChange | |
250 | where | |
251 | F: FnOnce() -> Vec<String>, | |
252 | { | |
253 | let contents = fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from {}: {}", path.display(), e)); | |
254 | let file_change = replace_region_in_text(&contents, start, end, replace_start, replacements); | |
255 | ||
256 | if write_back { | |
257 | if let Err(e) = fs::write(path, file_change.new_lines.as_bytes()) { | |
258 | panic!("Cannot write to {}: {}", path.display(), e); | |
259 | } | |
260 | } | |
261 | file_change | |
262 | } | |
263 | ||
264 | /// Replaces a region in a text delimited by two lines matching regexes. | |
265 | /// | |
266 | /// * `text` is the input text on which you want to perform the replacement | |
267 | /// * `start` is a `&str` that describes the delimiter line before the region you want to replace. | |
268 | /// As the `&str` will be converted to a `Regex`, this can contain regex syntax, too. | |
269 | /// * `end` is a `&str` that describes the delimiter line until where the replacement should happen. | |
270 | /// As the `&str` will be converted to a `Regex`, this can contain regex syntax, too. | |
271 | /// * If `replace_start` is true, the `start` delimiter line is replaced as well. The `end` | |
272 | /// delimiter line is never replaced. | |
273 | /// * `replacements` is a closure that has to return a `Vec<String>` which contains the new text. | |
274 | /// | |
275 | /// If you want to perform the replacement on files instead of already parsed text, | |
276 | /// use `replace_region_in_file`. | |
277 | /// | |
278 | /// # Example | |
279 | /// | |
280 | /// ``` | |
281 | /// let the_text = "replace_start\nsome text\nthat will be replaced\nreplace_end"; | |
282 | /// let result = | |
283 | /// clippy_dev::replace_region_in_text(the_text, "replace_start", "replace_end", false, || { | |
284 | /// vec!["a different".to_string(), "text".to_string()] | |
285 | /// }) | |
286 | /// .new_lines; | |
287 | /// assert_eq!("replace_start\na different\ntext\nreplace_end", result); | |
288 | /// ``` | |
289 | /// | |
290 | /// # Panics | |
291 | /// | |
292 | /// Panics if start or end is not valid regex | |
293 | pub fn replace_region_in_text<F>(text: &str, start: &str, end: &str, replace_start: bool, replacements: F) -> FileChange | |
294 | where | |
295 | F: FnOnce() -> Vec<String>, | |
296 | { | |
297 | let replace_it = replacements(); | |
298 | let mut in_old_region = false; | |
299 | let mut found = false; | |
300 | let mut new_lines = vec![]; | |
301 | let start = Regex::new(start).unwrap(); | |
302 | let end = Regex::new(end).unwrap(); | |
303 | ||
304 | for line in text.lines() { | |
305 | if in_old_region { | |
306 | if end.is_match(line) { | |
307 | in_old_region = false; | |
308 | new_lines.extend(replace_it.clone()); | |
309 | new_lines.push(line.to_string()); | |
310 | } | |
311 | } else if start.is_match(line) { | |
312 | if !replace_start { | |
313 | new_lines.push(line.to_string()); | |
314 | } | |
315 | in_old_region = true; | |
316 | found = true; | |
317 | } else { | |
318 | new_lines.push(line.to_string()); | |
319 | } | |
320 | } | |
321 | ||
322 | if !found { | |
323 | // This happens if the provided regex in `clippy_dev/src/main.rs` does not match in the | |
324 | // given text or file. Most likely this is an error on the programmer's side and the Regex | |
325 | // is incorrect. | |
326 | eprintln!("error: regex \n{:?}\ndoesn't match. You may have to update it.", start); | |
327 | std::process::exit(1); | |
328 | } | |
329 | ||
330 | let mut new_lines = new_lines.join("\n"); | |
331 | if text.ends_with('\n') { | |
332 | new_lines.push('\n'); | |
333 | } | |
334 | let changed = new_lines != text; | |
335 | FileChange { changed, new_lines } | |
336 | } | |
337 | ||
338 | /// Returns the path to the Clippy project directory | |
339 | /// | |
340 | /// # Panics | |
341 | /// | |
342 | /// Panics if the current directory could not be retrieved, there was an error reading any of the | |
343 | /// Cargo.toml files or ancestor directory is the clippy root directory | |
344 | #[must_use] | |
345 | pub fn clippy_project_root() -> PathBuf { | |
346 | let current_dir = std::env::current_dir().unwrap(); | |
347 | for path in current_dir.ancestors() { | |
348 | let result = std::fs::read_to_string(path.join("Cargo.toml")); | |
349 | if let Err(err) = &result { | |
350 | if err.kind() == std::io::ErrorKind::NotFound { | |
351 | continue; | |
352 | } | |
353 | } | |
354 | ||
355 | let content = result.unwrap(); | |
356 | if content.contains("[package]\nname = \"clippy\"") { | |
357 | return path.to_path_buf(); | |
358 | } | |
359 | } | |
360 | panic!("error: Can't determine root of project. Please run inside a Clippy working dir."); | |
361 | } | |
362 | ||
363 | #[test] | |
364 | fn test_parse_contents() { | |
365 | let result: Vec<Lint> = parse_contents( | |
366 | r#" | |
367 | declare_clippy_lint! { | |
368 | pub PTR_ARG, | |
369 | style, | |
370 | "really long \ | |
371 | text" | |
372 | } | |
373 | ||
374 | declare_clippy_lint!{ | |
375 | pub DOC_MARKDOWN, | |
376 | pedantic, | |
377 | "single line" | |
378 | } | |
379 | ||
380 | /// some doc comment | |
381 | declare_deprecated_lint! { | |
382 | pub SHOULD_ASSERT_EQ, | |
383 | "`assert!()` will be more flexible with RFC 2011" | |
384 | } | |
385 | "#, | |
386 | "module_name", | |
387 | ) | |
388 | .collect(); | |
389 | ||
390 | let expected = vec![ | |
391 | Lint::new("ptr_arg", "style", "really long text", None, "module_name"), | |
392 | Lint::new("doc_markdown", "pedantic", "single line", None, "module_name"), | |
393 | Lint::new( | |
394 | "should_assert_eq", | |
395 | "Deprecated", | |
396 | "`assert!()` will be more flexible with RFC 2011", | |
397 | Some("`assert!()` will be more flexible with RFC 2011"), | |
398 | "module_name", | |
399 | ), | |
400 | ]; | |
401 | assert_eq!(expected, result); | |
402 | } | |
403 | ||
404 | #[test] | |
405 | fn test_replace_region() { | |
406 | let text = "\nabc\n123\n789\ndef\nghi"; | |
407 | let expected = FileChange { | |
408 | changed: true, | |
409 | new_lines: "\nabc\nhello world\ndef\nghi".to_string(), | |
410 | }; | |
411 | let result = replace_region_in_text(text, r#"^\s*abc$"#, r#"^\s*def"#, false, || { | |
412 | vec!["hello world".to_string()] | |
413 | }); | |
414 | assert_eq!(expected, result); | |
415 | } | |
416 | ||
417 | #[test] | |
418 | fn test_replace_region_with_start() { | |
419 | let text = "\nabc\n123\n789\ndef\nghi"; | |
420 | let expected = FileChange { | |
421 | changed: true, | |
422 | new_lines: "\nhello world\ndef\nghi".to_string(), | |
423 | }; | |
424 | let result = replace_region_in_text(text, r#"^\s*abc$"#, r#"^\s*def"#, true, || { | |
425 | vec!["hello world".to_string()] | |
426 | }); | |
427 | assert_eq!(expected, result); | |
428 | } | |
429 | ||
430 | #[test] | |
431 | fn test_replace_region_no_changes() { | |
432 | let text = "123\n456\n789"; | |
433 | let expected = FileChange { | |
434 | changed: false, | |
435 | new_lines: "123\n456\n789".to_string(), | |
436 | }; | |
437 | let result = replace_region_in_text(text, r#"^\s*123$"#, r#"^\s*456"#, false, Vec::new); | |
438 | assert_eq!(expected, result); | |
439 | } | |
440 | ||
441 | #[test] | |
442 | fn test_usable_lints() { | |
443 | let lints = vec![ | |
444 | Lint::new("should_assert_eq", "Deprecated", "abc", Some("Reason"), "module_name"), | |
445 | Lint::new("should_assert_eq2", "Not Deprecated", "abc", None, "module_name"), | |
446 | Lint::new("should_assert_eq2", "internal", "abc", None, "module_name"), | |
447 | Lint::new("should_assert_eq2", "internal_style", "abc", None, "module_name"), | |
448 | ]; | |
449 | let expected = vec![Lint::new( | |
450 | "should_assert_eq2", | |
451 | "Not Deprecated", | |
452 | "abc", | |
453 | None, | |
454 | "module_name", | |
455 | )]; | |
456 | assert_eq!(expected, Lint::usable_lints(&lints)); | |
457 | } | |
458 | ||
459 | #[test] | |
460 | fn test_by_lint_group() { | |
461 | let lints = vec![ | |
462 | Lint::new("should_assert_eq", "group1", "abc", None, "module_name"), | |
463 | Lint::new("should_assert_eq2", "group2", "abc", None, "module_name"), | |
464 | Lint::new("incorrect_match", "group1", "abc", None, "module_name"), | |
465 | ]; | |
466 | let mut expected: HashMap<String, Vec<Lint>> = HashMap::new(); | |
467 | expected.insert( | |
468 | "group1".to_string(), | |
469 | vec![ | |
470 | Lint::new("should_assert_eq", "group1", "abc", None, "module_name"), | |
471 | Lint::new("incorrect_match", "group1", "abc", None, "module_name"), | |
472 | ], | |
473 | ); | |
474 | expected.insert( | |
475 | "group2".to_string(), | |
476 | vec![Lint::new("should_assert_eq2", "group2", "abc", None, "module_name")], | |
477 | ); | |
478 | assert_eq!(expected, Lint::by_lint_group(lints.into_iter())); | |
479 | } | |
480 | ||
481 | #[test] | |
482 | fn test_gen_changelog_lint_list() { | |
483 | let lints = vec![ | |
484 | Lint::new("should_assert_eq", "group1", "abc", None, "module_name"), | |
485 | Lint::new("should_assert_eq2", "group2", "abc", None, "module_name"), | |
486 | ]; | |
487 | let expected = vec![ | |
488 | format!("[`should_assert_eq`]: {}#should_assert_eq", DOCS_LINK.to_string()), | |
489 | format!("[`should_assert_eq2`]: {}#should_assert_eq2", DOCS_LINK.to_string()), | |
490 | ]; | |
491 | assert_eq!(expected, gen_changelog_lint_list(lints.iter())); | |
492 | } | |
493 | ||
494 | #[test] | |
495 | fn test_gen_deprecated() { | |
496 | let lints = vec![ | |
497 | Lint::new( | |
498 | "should_assert_eq", | |
499 | "group1", | |
500 | "abc", | |
501 | Some("has been superseded by should_assert_eq2"), | |
502 | "module_name", | |
503 | ), | |
504 | Lint::new( | |
505 | "another_deprecated", | |
506 | "group2", | |
507 | "abc", | |
508 | Some("will be removed"), | |
509 | "module_name", | |
510 | ), | |
511 | ]; | |
512 | let expected: Vec<String> = vec![ | |
513 | " store.register_removed(", | |
514 | " \"clippy::should_assert_eq\",", | |
515 | " \"has been superseded by should_assert_eq2\",", | |
516 | " );", | |
517 | " store.register_removed(", | |
518 | " \"clippy::another_deprecated\",", | |
519 | " \"will be removed\",", | |
520 | " );", | |
521 | ] | |
522 | .into_iter() | |
523 | .map(String::from) | |
524 | .collect(); | |
525 | assert_eq!(expected, gen_deprecated(lints.iter())); | |
526 | } | |
527 | ||
528 | #[test] | |
529 | #[should_panic] | |
530 | fn test_gen_deprecated_fail() { | |
531 | let lints = vec![Lint::new("should_assert_eq2", "group2", "abc", None, "module_name")]; | |
532 | let _deprecated_lints = gen_deprecated(lints.iter()); | |
533 | } | |
534 | ||
535 | #[test] | |
536 | fn test_gen_modules_list() { | |
537 | let lints = vec![ | |
538 | Lint::new("should_assert_eq", "group1", "abc", None, "module_name"), | |
539 | Lint::new("incorrect_stuff", "group3", "abc", None, "another_module"), | |
540 | ]; | |
541 | let expected = vec!["mod another_module;".to_string(), "mod module_name;".to_string()]; | |
542 | assert_eq!(expected, gen_modules_list(lints.iter())); | |
543 | } | |
544 | ||
545 | #[test] | |
546 | fn test_gen_lint_group_list() { | |
547 | let lints = vec![ | |
548 | Lint::new("abc", "group1", "abc", None, "module_name"), | |
549 | Lint::new("should_assert_eq", "group1", "abc", None, "module_name"), | |
550 | Lint::new("internal", "internal_style", "abc", None, "module_name"), | |
551 | ]; | |
552 | let expected = vec![ | |
553 | " LintId::of(&module_name::ABC),".to_string(), | |
554 | " LintId::of(&module_name::INTERNAL),".to_string(), | |
555 | " LintId::of(&module_name::SHOULD_ASSERT_EQ),".to_string(), | |
556 | ]; | |
557 | assert_eq!(expected, gen_lint_group_list(lints.iter())); | |
558 | } |