]> git.proxmox.com Git - rustc.git/blame - src/tools/clippy/clippy_dev/src/update_lints.rs
New upstream version 1.63.0+dfsg1
[rustc.git] / src / tools / clippy / clippy_dev / src / update_lints.rs
CommitLineData
04454e1e
FG
1use aho_corasick::AhoCorasickBuilder;
2use core::fmt::Write as _;
c295e0f8 3use itertools::Itertools;
04454e1e
FG
4use rustc_lexer::{tokenize, unescape, LiteralKind, TokenKind};
5use std::collections::{HashMap, HashSet};
c295e0f8
XL
6use std::ffi::OsStr;
7use std::fs;
04454e1e
FG
8use std::io::{self, Read as _, Seek as _, Write as _};
9use std::path::{Path, PathBuf};
10use walkdir::{DirEntry, WalkDir};
c295e0f8
XL
11
12use crate::clippy_project_root;
13
14const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\
15 // Use that command to update this file and do not edit by hand.\n\
16 // Manual edits will be overwritten.\n\n";
17
04454e1e 18const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html";
f20569fa 19
923072b8 20#[derive(Clone, Copy, PartialEq, Eq)]
f20569fa
XL
21pub enum UpdateMode {
22 Check,
23 Change,
24}
25
c295e0f8
XL
26/// Runs the `update_lints` command.
27///
28/// This updates various generated values from the lint source code.
29///
30/// `update_mode` indicates if the files should be updated or if updates should be checked for.
31///
32/// # Panics
33///
34/// Panics if a file path could not read from or then written to
04454e1e
FG
35pub fn update(update_mode: UpdateMode) {
36 let (lints, deprecated_lints, renamed_lints) = gather_all();
37 generate_lint_files(update_mode, &lints, &deprecated_lints, &renamed_lints);
38}
f20569fa 39
04454e1e
FG
40fn generate_lint_files(
41 update_mode: UpdateMode,
42 lints: &[Lint],
43 deprecated_lints: &[DeprecatedLint],
44 renamed_lints: &[RenamedLint],
45) {
46 let internal_lints = Lint::internal_lints(lints);
47 let usable_lints = Lint::usable_lints(lints);
f20569fa
XL
48 let mut sorted_usable_lints = usable_lints.clone();
49 sorted_usable_lints.sort_by_key(|lint| lint.name.clone());
50
04454e1e
FG
51 replace_region_in_file(
52 update_mode,
f20569fa 53 Path::new("README.md"),
04454e1e
FG
54 "[There are over ",
55 " lints included in this crate!]",
56 |res| {
57 write!(res, "{}", round_to_fifty(usable_lints.len())).unwrap();
f20569fa 58 },
04454e1e 59 );
f20569fa 60
923072b8
FG
61 replace_region_in_file(
62 update_mode,
63 Path::new("book/src/README.md"),
64 "[There are over ",
65 " lints included in this crate!]",
66 |res| {
67 write!(res, "{}", round_to_fifty(usable_lints.len())).unwrap();
68 },
69 );
70
04454e1e
FG
71 replace_region_in_file(
72 update_mode,
f20569fa 73 Path::new("CHANGELOG.md"),
04454e1e 74 "<!-- begin autogenerated links to lint list -->\n",
f20569fa 75 "<!-- end autogenerated links to lint list -->",
04454e1e
FG
76 |res| {
77 for lint in usable_lints
78 .iter()
923072b8
FG
79 .map(|l| &*l.name)
80 .chain(deprecated_lints.iter().map(|l| &*l.name))
81 .chain(
82 renamed_lints
83 .iter()
84 .map(|l| l.old_name.strip_prefix("clippy::").unwrap_or(&l.old_name)),
85 )
04454e1e
FG
86 .sorted()
87 {
88 writeln!(res, "[`{}`]: {}#{}", lint, DOCS_LINK, lint).unwrap();
89 }
90 },
91 );
f20569fa 92
c295e0f8 93 // This has to be in lib.rs, otherwise rustfmt doesn't work
04454e1e
FG
94 replace_region_in_file(
95 update_mode,
f20569fa 96 Path::new("clippy_lints/src/lib.rs"),
04454e1e
FG
97 "// begin lints modules, do not remove this comment, it’s used in `update_lints`\n",
98 "// end lints modules, do not remove this comment, it’s used in `update_lints`",
99 |res| {
100 for lint_mod in usable_lints.iter().map(|l| &l.module).unique().sorted() {
101 writeln!(res, "mod {};", lint_mod).unwrap();
102 }
103 },
104 );
f20569fa 105
c295e0f8
XL
106 process_file(
107 "clippy_lints/src/lib.register_lints.rs",
108 update_mode,
109 &gen_register_lint_list(internal_lints.iter(), usable_lints.iter()),
110 );
111 process_file(
112 "clippy_lints/src/lib.deprecated.rs",
113 update_mode,
04454e1e 114 &gen_deprecated(deprecated_lints),
c295e0f8
XL
115 );
116
117 let all_group_lints = usable_lints.iter().filter(|l| {
118 matches!(
119 &*l.group,
120 "correctness" | "suspicious" | "style" | "complexity" | "perf"
f20569fa 121 )
c295e0f8
XL
122 });
123 let content = gen_lint_group_list("all", all_group_lints);
124 process_file("clippy_lints/src/lib.register_all.rs", update_mode, &content);
f20569fa 125
c295e0f8
XL
126 for (lint_group, lints) in Lint::by_lint_group(usable_lints.into_iter().chain(internal_lints)) {
127 let content = gen_lint_group_list(&lint_group, lints.iter());
128 process_file(
129 &format!("clippy_lints/src/lib.register_{}.rs", lint_group),
130 update_mode,
131 &content,
f20569fa 132 );
f20569fa 133 }
04454e1e
FG
134
135 let content = gen_deprecated_lints_test(deprecated_lints);
136 process_file("tests/ui/deprecated.rs", update_mode, &content);
137
138 let content = gen_renamed_lints_test(renamed_lints);
139 process_file("tests/ui/rename.rs", update_mode, &content);
f20569fa
XL
140}
141
142pub fn print_lints() {
04454e1e 143 let (lint_list, _, _) = gather_all();
f20569fa
XL
144 let usable_lints = Lint::usable_lints(&lint_list);
145 let usable_lint_count = usable_lints.len();
146 let grouped_by_lint_group = Lint::by_lint_group(usable_lints.into_iter());
147
148 for (lint_group, mut lints) in grouped_by_lint_group {
f20569fa
XL
149 println!("\n## {}", lint_group);
150
151 lints.sort_by_key(|l| l.name.clone());
152
153 for lint in lints {
154 println!("* [{}]({}#{}) ({})", lint.name, DOCS_LINK, lint.name, lint.desc);
155 }
156 }
157
158 println!("there are {} lints", usable_lint_count);
159}
160
04454e1e
FG
161/// Runs the `rename_lint` command.
162///
163/// This does the following:
164/// * Adds an entry to `renamed_lints.rs`.
165/// * Renames all lint attributes to the new name (e.g. `#[allow(clippy::lint_name)]`).
166/// * Renames the lint struct to the new name.
167/// * Renames the module containing the lint struct to the new name if it shares a name with the
168/// lint.
169///
170/// # Panics
171/// Panics for the following conditions:
172/// * If a file path could not read from or then written to
173/// * If either lint name has a prefix
174/// * If `old_name` doesn't name an existing lint.
175/// * If `old_name` names a deprecated or renamed lint.
176#[allow(clippy::too_many_lines)]
177pub fn rename(old_name: &str, new_name: &str, uplift: bool) {
178 if let Some((prefix, _)) = old_name.split_once("::") {
179 panic!("`{}` should not contain the `{}` prefix", old_name, prefix);
180 }
181 if let Some((prefix, _)) = new_name.split_once("::") {
182 panic!("`{}` should not contain the `{}` prefix", new_name, prefix);
183 }
184
185 let (mut lints, deprecated_lints, mut renamed_lints) = gather_all();
186 let mut old_lint_index = None;
187 let mut found_new_name = false;
188 for (i, lint) in lints.iter().enumerate() {
189 if lint.name == old_name {
190 old_lint_index = Some(i);
191 } else if lint.name == new_name {
192 found_new_name = true;
193 }
194 }
195 let old_lint_index = old_lint_index.unwrap_or_else(|| panic!("could not find lint `{}`", old_name));
196
197 let lint = RenamedLint {
198 old_name: format!("clippy::{}", old_name),
199 new_name: if uplift {
200 new_name.into()
201 } else {
202 format!("clippy::{}", new_name)
203 },
204 };
205
206 // Renamed lints and deprecated lints shouldn't have been found in the lint list, but check just in
207 // case.
208 assert!(
209 !renamed_lints.iter().any(|l| lint.old_name == l.old_name),
210 "`{}` has already been renamed",
211 old_name
212 );
213 assert!(
214 !deprecated_lints.iter().any(|l| lint.old_name == l.name),
215 "`{}` has already been deprecated",
216 old_name
217 );
218
219 // Update all lint level attributes. (`clippy::lint_name`)
220 for file in WalkDir::new(clippy_project_root())
221 .into_iter()
222 .map(Result::unwrap)
223 .filter(|f| {
224 let name = f.path().file_name();
225 let ext = f.path().extension();
226 (ext == Some(OsStr::new("rs")) || ext == Some(OsStr::new("fixed")))
227 && name != Some(OsStr::new("rename.rs"))
228 && name != Some(OsStr::new("renamed_lints.rs"))
229 })
230 {
231 rewrite_file(file.path(), |s| {
232 replace_ident_like(s, &[(&lint.old_name, &lint.new_name)])
233 });
234 }
235
236 renamed_lints.push(lint);
237 renamed_lints.sort_by(|lhs, rhs| {
238 lhs.new_name
239 .starts_with("clippy::")
240 .cmp(&rhs.new_name.starts_with("clippy::"))
241 .reverse()
242 .then_with(|| lhs.old_name.cmp(&rhs.old_name))
243 });
244
245 write_file(
246 Path::new("clippy_lints/src/renamed_lints.rs"),
247 &gen_renamed_lints_list(&renamed_lints),
248 );
249
250 if uplift {
251 write_file(Path::new("tests/ui/rename.rs"), &gen_renamed_lints_test(&renamed_lints));
252 println!(
253 "`{}` has be uplifted. All the code inside `clippy_lints` related to it needs to be removed manually.",
254 old_name
255 );
256 } else if found_new_name {
257 write_file(Path::new("tests/ui/rename.rs"), &gen_renamed_lints_test(&renamed_lints));
258 println!(
259 "`{}` is already defined. The old linting code inside `clippy_lints` needs to be updated/removed manually.",
260 new_name
261 );
262 } else {
263 // Rename the lint struct and source files sharing a name with the lint.
264 let lint = &mut lints[old_lint_index];
265 let old_name_upper = old_name.to_uppercase();
266 let new_name_upper = new_name.to_uppercase();
267 lint.name = new_name.into();
268
269 // Rename test files. only rename `.stderr` and `.fixed` files if the new test name doesn't exist.
270 if try_rename_file(
271 Path::new(&format!("tests/ui/{}.rs", old_name)),
272 Path::new(&format!("tests/ui/{}.rs", new_name)),
273 ) {
274 try_rename_file(
275 Path::new(&format!("tests/ui/{}.stderr", old_name)),
276 Path::new(&format!("tests/ui/{}.stderr", new_name)),
277 );
278 try_rename_file(
279 Path::new(&format!("tests/ui/{}.fixed", old_name)),
280 Path::new(&format!("tests/ui/{}.fixed", new_name)),
281 );
282 }
283
284 // Try to rename the file containing the lint if the file name matches the lint's name.
285 let replacements;
286 let replacements = if lint.module == old_name
287 && try_rename_file(
288 Path::new(&format!("clippy_lints/src/{}.rs", old_name)),
289 Path::new(&format!("clippy_lints/src/{}.rs", new_name)),
290 ) {
291 // Edit the module name in the lint list. Note there could be multiple lints.
292 for lint in lints.iter_mut().filter(|l| l.module == old_name) {
293 lint.module = new_name.into();
294 }
295 replacements = [(&*old_name_upper, &*new_name_upper), (old_name, new_name)];
296 replacements.as_slice()
297 } else if !lint.module.contains("::")
298 // Catch cases like `methods/lint_name.rs` where the lint is stored in `methods/mod.rs`
299 && try_rename_file(
300 Path::new(&format!("clippy_lints/src/{}/{}.rs", lint.module, old_name)),
301 Path::new(&format!("clippy_lints/src/{}/{}.rs", lint.module, new_name)),
302 )
303 {
304 // Edit the module name in the lint list. Note there could be multiple lints, or none.
305 let renamed_mod = format!("{}::{}", lint.module, old_name);
306 for lint in lints.iter_mut().filter(|l| l.module == renamed_mod) {
307 lint.module = format!("{}::{}", lint.module, new_name);
308 }
309 replacements = [(&*old_name_upper, &*new_name_upper), (old_name, new_name)];
310 replacements.as_slice()
311 } else {
312 replacements = [(&*old_name_upper, &*new_name_upper), ("", "")];
313 &replacements[0..1]
314 };
315
316 // Don't change `clippy_utils/src/renamed_lints.rs` here as it would try to edit the lint being
317 // renamed.
318 for (_, file) in clippy_lints_src_files().filter(|(rel_path, _)| rel_path != OsStr::new("renamed_lints.rs")) {
319 rewrite_file(file.path(), |s| replace_ident_like(s, replacements));
320 }
321
322 generate_lint_files(UpdateMode::Change, &lints, &deprecated_lints, &renamed_lints);
323 println!("{} has been successfully renamed", old_name);
324 }
325
326 println!("note: `cargo uitest` still needs to be run to update the test results");
327}
328
329/// Replace substrings if they aren't bordered by identifier characters. Returns `None` if there
330/// were no replacements.
331fn replace_ident_like(contents: &str, replacements: &[(&str, &str)]) -> Option<String> {
332 fn is_ident_char(c: u8) -> bool {
333 matches!(c, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')
334 }
335
336 let searcher = AhoCorasickBuilder::new()
337 .dfa(true)
338 .match_kind(aho_corasick::MatchKind::LeftmostLongest)
339 .build_with_size::<u16, _, _>(replacements.iter().map(|&(x, _)| x.as_bytes()))
340 .unwrap();
341
342 let mut result = String::with_capacity(contents.len() + 1024);
343 let mut pos = 0;
344 let mut edited = false;
345 for m in searcher.find_iter(contents) {
346 let (old, new) = replacements[m.pattern()];
347 result.push_str(&contents[pos..m.start()]);
348 result.push_str(
349 if !is_ident_char(contents.as_bytes().get(m.start().wrapping_sub(1)).copied().unwrap_or(0))
350 && !is_ident_char(contents.as_bytes().get(m.end()).copied().unwrap_or(0))
351 {
352 edited = true;
353 new
354 } else {
355 old
356 },
357 );
358 pos = m.end();
359 }
360 result.push_str(&contents[pos..]);
361 edited.then(|| result)
362}
363
f20569fa
XL
364fn round_to_fifty(count: usize) -> usize {
365 count / 50 * 50
366}
c295e0f8
XL
367
368fn process_file(path: impl AsRef<Path>, update_mode: UpdateMode, content: &str) {
369 if update_mode == UpdateMode::Check {
370 let old_content =
371 fs::read_to_string(&path).unwrap_or_else(|e| panic!("Cannot read from {}: {}", path.as_ref().display(), e));
372 if content != old_content {
373 exit_with_failure();
374 }
375 } else {
376 fs::write(&path, content.as_bytes())
377 .unwrap_or_else(|e| panic!("Cannot write to {}: {}", path.as_ref().display(), e));
378 }
379}
380
381fn exit_with_failure() {
382 println!(
383 "Not all lints defined properly. \
384 Please run `cargo dev update_lints` to make sure all lints are defined properly."
385 );
386 std::process::exit(1);
387}
388
389/// Lint data parsed from the Clippy source code.
923072b8 390#[derive(Clone, PartialEq, Eq, Debug)]
c295e0f8
XL
391struct Lint {
392 name: String,
393 group: String,
394 desc: String,
c295e0f8
XL
395 module: String,
396}
397
398impl Lint {
399 #[must_use]
04454e1e 400 fn new(name: &str, group: &str, desc: &str, module: &str) -> Self {
c295e0f8
XL
401 Self {
402 name: name.to_lowercase(),
04454e1e
FG
403 group: group.into(),
404 desc: remove_line_splices(desc),
405 module: module.into(),
c295e0f8
XL
406 }
407 }
408
409 /// Returns all non-deprecated lints and non-internal lints
410 #[must_use]
411 fn usable_lints(lints: &[Self]) -> Vec<Self> {
412 lints
413 .iter()
04454e1e 414 .filter(|l| !l.group.starts_with("internal"))
c295e0f8
XL
415 .cloned()
416 .collect()
417 }
418
419 /// Returns all internal lints (not `internal_warn` lints)
420 #[must_use]
421 fn internal_lints(lints: &[Self]) -> Vec<Self> {
422 lints.iter().filter(|l| l.group == "internal").cloned().collect()
423 }
424
c295e0f8
XL
425 /// Returns the lints in a `HashMap`, grouped by the different lint groups
426 #[must_use]
427 fn by_lint_group(lints: impl Iterator<Item = Self>) -> HashMap<String, Vec<Self>> {
428 lints.map(|lint| (lint.group.to_string(), lint)).into_group_map()
429 }
430}
431
923072b8 432#[derive(Clone, PartialEq, Eq, Debug)]
04454e1e
FG
433struct DeprecatedLint {
434 name: String,
435 reason: String,
436}
437impl DeprecatedLint {
438 fn new(name: &str, reason: &str) -> Self {
439 Self {
440 name: name.to_lowercase(),
441 reason: remove_line_splices(reason),
442 }
443 }
444}
445
446struct RenamedLint {
447 old_name: String,
448 new_name: String,
449}
450impl RenamedLint {
451 fn new(old_name: &str, new_name: &str) -> Self {
452 Self {
453 old_name: remove_line_splices(old_name),
454 new_name: remove_line_splices(new_name),
455 }
456 }
457}
458
c295e0f8
XL
459/// Generates the code for registering a group
460fn gen_lint_group_list<'a>(group_name: &str, lints: impl Iterator<Item = &'a Lint>) -> String {
461 let mut details: Vec<_> = lints.map(|l| (&l.module, l.name.to_uppercase())).collect();
462 details.sort_unstable();
463
464 let mut output = GENERATED_FILE_COMMENT.to_string();
465
04454e1e
FG
466 let _ = writeln!(
467 output,
468 "store.register_group(true, \"clippy::{0}\", Some(\"clippy_{0}\"), vec![",
c295e0f8 469 group_name
04454e1e 470 );
c295e0f8 471 for (module, name) in details {
04454e1e 472 let _ = writeln!(output, " LintId::of({}::{}),", module, name);
c295e0f8
XL
473 }
474 output.push_str("])\n");
475
476 output
477}
478
c295e0f8
XL
479/// Generates the `register_removed` code
480#[must_use]
04454e1e 481fn gen_deprecated(lints: &[DeprecatedLint]) -> String {
c295e0f8
XL
482 let mut output = GENERATED_FILE_COMMENT.to_string();
483 output.push_str("{\n");
04454e1e
FG
484 for lint in lints {
485 let _ = write!(
486 output,
c295e0f8
XL
487 concat!(
488 " store.register_removed(\n",
489 " \"clippy::{}\",\n",
490 " \"{}\",\n",
491 " );\n"
492 ),
04454e1e
FG
493 lint.name, lint.reason,
494 );
c295e0f8
XL
495 }
496 output.push_str("}\n");
497
498 output
499}
500
501/// Generates the code for registering lints
502#[must_use]
503fn gen_register_lint_list<'a>(
504 internal_lints: impl Iterator<Item = &'a Lint>,
505 usable_lints: impl Iterator<Item = &'a Lint>,
506) -> String {
507 let mut details: Vec<_> = internal_lints
508 .map(|l| (false, &l.module, l.name.to_uppercase()))
509 .chain(usable_lints.map(|l| (true, &l.module, l.name.to_uppercase())))
510 .collect();
511 details.sort_unstable();
512
513 let mut output = GENERATED_FILE_COMMENT.to_string();
514 output.push_str("store.register_lints(&[\n");
515
516 for (is_public, module_name, lint_name) in details {
517 if !is_public {
5099ac24 518 output.push_str(" #[cfg(feature = \"internal\")]\n");
c295e0f8 519 }
04454e1e 520 let _ = writeln!(output, " {}::{},", module_name, lint_name);
c295e0f8
XL
521 }
522 output.push_str("])\n");
523
524 output
525}
526
04454e1e
FG
527fn gen_deprecated_lints_test(lints: &[DeprecatedLint]) -> String {
528 let mut res: String = GENERATED_FILE_COMMENT.into();
529 for lint in lints {
530 writeln!(res, "#![warn(clippy::{})]", lint.name).unwrap();
531 }
532 res.push_str("\nfn main() {}\n");
533 res
c295e0f8
XL
534}
535
04454e1e
FG
536fn gen_renamed_lints_test(lints: &[RenamedLint]) -> String {
537 let mut seen_lints = HashSet::new();
538 let mut res: String = GENERATED_FILE_COMMENT.into();
539 res.push_str("// run-rustfix\n\n");
540 for lint in lints {
541 if seen_lints.insert(&lint.new_name) {
542 writeln!(res, "#![allow({})]", lint.new_name).unwrap();
543 }
544 }
545 seen_lints.clear();
546 for lint in lints {
547 if seen_lints.insert(&lint.old_name) {
548 writeln!(res, "#![warn({})]", lint.old_name).unwrap();
549 }
c295e0f8 550 }
04454e1e
FG
551 res.push_str("\nfn main() {}\n");
552 res
553}
c295e0f8 554
04454e1e
FG
555fn gen_renamed_lints_list(lints: &[RenamedLint]) -> String {
556 const HEADER: &str = "\
557 // This file is managed by `cargo dev rename_lint`. Prefer using that when possible.\n\n\
558 #[rustfmt::skip]\n\
559 pub static RENAMED_LINTS: &[(&str, &str)] = &[\n";
c295e0f8 560
04454e1e
FG
561 let mut res = String::from(HEADER);
562 for lint in lints {
563 writeln!(res, " (\"{}\", \"{}\"),", lint.old_name, lint.new_name).unwrap();
564 }
565 res.push_str("];\n");
566 res
c295e0f8
XL
567}
568
04454e1e
FG
569/// Gathers all lints defined in `clippy_lints/src`
570fn gather_all() -> (Vec<Lint>, Vec<DeprecatedLint>, Vec<RenamedLint>) {
571 let mut lints = Vec::with_capacity(1000);
572 let mut deprecated_lints = Vec::with_capacity(50);
573 let mut renamed_lints = Vec::with_capacity(50);
574
575 for (rel_path, file) in clippy_lints_src_files() {
576 let path = file.path();
577 let contents =
578 fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from `{}`: {}", path.display(), e));
579 let module = rel_path
580 .components()
581 .map(|c| c.as_os_str().to_str().unwrap())
582 .collect::<Vec<_>>()
583 .join("::");
584
585 // If the lints are stored in mod.rs, we get the module name from
586 // the containing directory:
587 let module = if let Some(module) = module.strip_suffix("::mod.rs") {
588 module
589 } else {
590 module.strip_suffix(".rs").unwrap_or(&module)
591 };
592
593 match module {
594 "deprecated_lints" => parse_deprecated_contents(&contents, &mut deprecated_lints),
595 "renamed_lints" => parse_renamed_contents(&contents, &mut renamed_lints),
596 _ => parse_contents(&contents, module, &mut lints),
597 }
598 }
599 (lints, deprecated_lints, renamed_lints)
c295e0f8
XL
600}
601
04454e1e
FG
602fn clippy_lints_src_files() -> impl Iterator<Item = (PathBuf, DirEntry)> {
603 let root_path = clippy_project_root().join("clippy_lints/src");
604 let iter = WalkDir::new(&root_path).into_iter();
605 iter.map(Result::unwrap)
c295e0f8 606 .filter(|f| f.path().extension() == Some(OsStr::new("rs")))
04454e1e
FG
607 .map(move |f| (f.path().strip_prefix(&root_path).unwrap().to_path_buf(), f))
608}
609
610macro_rules! match_tokens {
611 ($iter:ident, $($token:ident $({$($fields:tt)*})? $(($capture:ident))?)*) => {
612 {
613 $($(let $capture =)? if let Some((TokenKind::$token $({$($fields)*})?, _x)) = $iter.next() {
614 _x
615 } else {
616 continue;
617 };)*
618 #[allow(clippy::unused_unit)]
619 { ($($($capture,)?)*) }
620 }
621 }
622}
623
624/// Parse a source file looking for `declare_clippy_lint` macro invocations.
625fn parse_contents(contents: &str, module: &str, lints: &mut Vec<Lint>) {
626 let mut offset = 0usize;
627 let mut iter = tokenize(contents).map(|t| {
628 let range = offset..offset + t.len;
629 offset = range.end;
630 (t.kind, &contents[range])
631 });
632
633 while iter.any(|(kind, s)| kind == TokenKind::Ident && s == "declare_clippy_lint") {
634 let mut iter = iter
635 .by_ref()
636 .filter(|&(kind, _)| !matches!(kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
637 // matches `!{`
638 match_tokens!(iter, Bang OpenBrace);
639 match iter.next() {
640 // #[clippy::version = "version"] pub
641 Some((TokenKind::Pound, _)) => {
642 match_tokens!(iter, OpenBracket Ident Colon Colon Ident Eq Literal{..} CloseBracket Ident);
643 },
644 // pub
645 Some((TokenKind::Ident, _)) => (),
646 _ => continue,
647 }
648 let (name, group, desc) = match_tokens!(
649 iter,
650 // LINT_NAME
651 Ident(name) Comma
652 // group,
653 Ident(group) Comma
654 // "description" }
655 Literal{..}(desc) CloseBrace
656 );
657 lints.push(Lint::new(name, group, desc, module));
658 }
659}
660
661/// Parse a source file looking for `declare_deprecated_lint` macro invocations.
662fn parse_deprecated_contents(contents: &str, lints: &mut Vec<DeprecatedLint>) {
663 let mut offset = 0usize;
664 let mut iter = tokenize(contents).map(|t| {
665 let range = offset..offset + t.len;
666 offset = range.end;
667 (t.kind, &contents[range])
668 });
669 while iter.any(|(kind, s)| kind == TokenKind::Ident && s == "declare_deprecated_lint") {
670 let mut iter = iter
671 .by_ref()
672 .filter(|&(kind, _)| !matches!(kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
673 let (name, reason) = match_tokens!(
674 iter,
675 // !{
676 Bang OpenBrace
677 // #[clippy::version = "version"]
678 Pound OpenBracket Ident Colon Colon Ident Eq Literal{..} CloseBracket
679 // pub LINT_NAME,
680 Ident Ident(name) Comma
681 // "description"
682 Literal{kind: LiteralKind::Str{..},..}(reason)
683 // }
684 CloseBrace
685 );
686 lints.push(DeprecatedLint::new(name, reason));
687 }
688}
689
690fn parse_renamed_contents(contents: &str, lints: &mut Vec<RenamedLint>) {
691 for line in contents.lines() {
692 let mut offset = 0usize;
693 let mut iter = tokenize(line).map(|t| {
694 let range = offset..offset + t.len;
695 offset = range.end;
696 (t.kind, &line[range])
697 });
698 let (old_name, new_name) = match_tokens!(
699 iter,
700 // ("old_name",
701 Whitespace OpenParen Literal{kind: LiteralKind::Str{..},..}(old_name) Comma
702 // "new_name"),
703 Whitespace Literal{kind: LiteralKind::Str{..},..}(new_name) CloseParen Comma
704 );
705 lints.push(RenamedLint::new(old_name, new_name));
706 }
c295e0f8
XL
707}
708
04454e1e
FG
709/// Removes the line splices and surrounding quotes from a string literal
710fn remove_line_splices(s: &str) -> String {
711 let s = s
712 .strip_prefix('r')
713 .unwrap_or(s)
714 .trim_matches('#')
715 .strip_prefix('"')
716 .and_then(|s| s.strip_suffix('"'))
717 .unwrap_or_else(|| panic!("expected quoted string, found `{}`", s));
718 let mut res = String::with_capacity(s.len());
719 unescape::unescape_literal(s, unescape::Mode::Str, &mut |range, _| res.push_str(&s[range]));
720 res
c295e0f8
XL
721}
722
723/// Replaces a region in a file delimited by two lines matching regexes.
724///
725/// `path` is the relative path to the file on which you want to perform the replacement.
726///
727/// See `replace_region_in_text` for documentation of the other options.
728///
729/// # Panics
730///
731/// Panics if the path could not read or then written
04454e1e
FG
732fn replace_region_in_file(
733 update_mode: UpdateMode,
c295e0f8
XL
734 path: &Path,
735 start: &str,
736 end: &str,
04454e1e
FG
737 write_replacement: impl FnMut(&mut String),
738) {
739 let contents = fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from `{}`: {}", path.display(), e));
740 let new_contents = match replace_region_in_text(&contents, start, end, write_replacement) {
741 Ok(x) => x,
742 Err(delim) => panic!("Couldn't find `{}` in file `{}`", delim, path.display()),
743 };
744
745 match update_mode {
746 UpdateMode::Check if contents != new_contents => exit_with_failure(),
747 UpdateMode::Check => (),
748 UpdateMode::Change => {
749 if let Err(e) = fs::write(path, new_contents.as_bytes()) {
750 panic!("Cannot write to `{}`: {}", path.display(), e);
c295e0f8 751 }
04454e1e 752 },
c295e0f8 753 }
04454e1e 754}
c295e0f8 755
04454e1e
FG
756/// Replaces a region in a text delimited by two strings. Returns the new text if both delimiters
757/// were found, or the missing delimiter if not.
758fn replace_region_in_text<'a>(
759 text: &str,
760 start: &'a str,
761 end: &'a str,
762 mut write_replacement: impl FnMut(&mut String),
763) -> Result<String, &'a str> {
764 let (text_start, rest) = text.split_once(start).ok_or(start)?;
765 let (_, text_end) = rest.split_once(end).ok_or(end)?;
766
767 let mut res = String::with_capacity(text.len() + 4096);
768 res.push_str(text_start);
769 res.push_str(start);
770 write_replacement(&mut res);
771 res.push_str(end);
772 res.push_str(text_end);
773
774 Ok(res)
c295e0f8
XL
775}
776
04454e1e
FG
777fn try_rename_file(old_name: &Path, new_name: &Path) -> bool {
778 match fs::OpenOptions::new().create_new(true).write(true).open(new_name) {
779 Ok(file) => drop(file),
780 Err(e) if matches!(e.kind(), io::ErrorKind::AlreadyExists | io::ErrorKind::NotFound) => return false,
781 Err(e) => panic_file(e, new_name, "create"),
782 };
783 match fs::rename(old_name, new_name) {
784 Ok(()) => true,
785 Err(e) => {
786 drop(fs::remove_file(new_name));
787 if e.kind() == io::ErrorKind::NotFound {
788 false
789 } else {
790 panic_file(e, old_name, "rename");
791 }
792 },
793 }
c295e0f8
XL
794}
795
04454e1e
FG
796#[allow(clippy::needless_pass_by_value)]
797fn panic_file(error: io::Error, name: &Path, action: &str) -> ! {
798 panic!("failed to {} file `{}`: {}", action, name.display(), error)
c295e0f8
XL
799}
800
04454e1e
FG
801fn rewrite_file(path: &Path, f: impl FnOnce(&str) -> Option<String>) {
802 let mut file = fs::OpenOptions::new()
803 .write(true)
804 .read(true)
805 .open(path)
806 .unwrap_or_else(|e| panic_file(e, path, "open"));
807 let mut buf = String::new();
808 file.read_to_string(&mut buf)
809 .unwrap_or_else(|e| panic_file(e, path, "read"));
810 if let Some(new_contents) = f(&buf) {
811 file.rewind().unwrap_or_else(|e| panic_file(e, path, "write"));
812 file.write_all(new_contents.as_bytes())
813 .unwrap_or_else(|e| panic_file(e, path, "write"));
814 file.set_len(new_contents.len() as u64)
815 .unwrap_or_else(|e| panic_file(e, path, "write"));
816 }
c295e0f8 817}
c295e0f8 818
04454e1e
FG
819fn write_file(path: &Path, contents: &str) {
820 fs::write(path, contents).unwrap_or_else(|e| panic_file(e, path, "write"));
c295e0f8
XL
821}
822
823#[cfg(test)]
824mod tests {
825 use super::*;
826
827 #[test]
04454e1e
FG
828 fn test_parse_contents() {
829 static CONTENTS: &str = r#"
830 declare_clippy_lint! {
831 #[clippy::version = "Hello Clippy!"]
832 pub PTR_ARG,
833 style,
834 "really long \
835 text"
836 }
c295e0f8 837
04454e1e
FG
838 declare_clippy_lint!{
839 #[clippy::version = "Test version"]
840 pub DOC_MARKDOWN,
841 pedantic,
842 "single line"
843 }
844 "#;
845 let mut result = Vec::new();
846 parse_contents(CONTENTS, "module_name", &mut result);
847
848 let expected = vec![
849 Lint::new("ptr_arg", "style", "\"really long text\"", "module_name"),
850 Lint::new("doc_markdown", "pedantic", "\"single line\"", "module_name"),
851 ];
c295e0f8
XL
852 assert_eq!(expected, result);
853 }
854
855 #[test]
04454e1e
FG
856 fn test_parse_deprecated_contents() {
857 static DEPRECATED_CONTENTS: &str = r#"
858 /// some doc comment
859 declare_deprecated_lint! {
860 #[clippy::version = "I'm a version"]
861 pub SHOULD_ASSERT_EQ,
862 "`assert!()` will be more flexible with RFC 2011"
863 }
864 "#;
865
866 let mut result = Vec::new();
867 parse_deprecated_contents(DEPRECATED_CONTENTS, &mut result);
868
869 let expected = vec![DeprecatedLint::new(
870 "should_assert_eq",
871 "\"`assert!()` will be more flexible with RFC 2011\"",
872 )];
c295e0f8
XL
873 assert_eq!(expected, result);
874 }
875
876 #[test]
877 fn test_usable_lints() {
878 let lints = vec![
04454e1e
FG
879 Lint::new("should_assert_eq2", "Not Deprecated", "\"abc\"", "module_name"),
880 Lint::new("should_assert_eq2", "internal", "\"abc\"", "module_name"),
881 Lint::new("should_assert_eq2", "internal_style", "\"abc\"", "module_name"),
c295e0f8
XL
882 ];
883 let expected = vec![Lint::new(
884 "should_assert_eq2",
885 "Not Deprecated",
04454e1e 886 "\"abc\"",
c295e0f8
XL
887 "module_name",
888 )];
889 assert_eq!(expected, Lint::usable_lints(&lints));
890 }
891
892 #[test]
893 fn test_by_lint_group() {
894 let lints = vec![
04454e1e
FG
895 Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name"),
896 Lint::new("should_assert_eq2", "group2", "\"abc\"", "module_name"),
897 Lint::new("incorrect_match", "group1", "\"abc\"", "module_name"),
c295e0f8
XL
898 ];
899 let mut expected: HashMap<String, Vec<Lint>> = HashMap::new();
900 expected.insert(
901 "group1".to_string(),
902 vec![
04454e1e
FG
903 Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name"),
904 Lint::new("incorrect_match", "group1", "\"abc\"", "module_name"),
c295e0f8
XL
905 ],
906 );
907 expected.insert(
908 "group2".to_string(),
04454e1e 909 vec![Lint::new("should_assert_eq2", "group2", "\"abc\"", "module_name")],
c295e0f8
XL
910 );
911 assert_eq!(expected, Lint::by_lint_group(lints.into_iter()));
912 }
913
c295e0f8
XL
914 #[test]
915 fn test_gen_deprecated() {
916 let lints = vec![
04454e1e
FG
917 DeprecatedLint::new("should_assert_eq", "\"has been superseded by should_assert_eq2\""),
918 DeprecatedLint::new("another_deprecated", "\"will be removed\""),
c295e0f8
XL
919 ];
920
921 let expected = GENERATED_FILE_COMMENT.to_string()
922 + &[
923 "{",
924 " store.register_removed(",
925 " \"clippy::should_assert_eq\",",
926 " \"has been superseded by should_assert_eq2\",",
927 " );",
928 " store.register_removed(",
929 " \"clippy::another_deprecated\",",
930 " \"will be removed\",",
931 " );",
932 "}",
933 ]
934 .join("\n")
935 + "\n";
936
04454e1e 937 assert_eq!(expected, gen_deprecated(&lints));
c295e0f8
XL
938 }
939
940 #[test]
941 fn test_gen_lint_group_list() {
942 let lints = vec![
04454e1e
FG
943 Lint::new("abc", "group1", "\"abc\"", "module_name"),
944 Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name"),
945 Lint::new("internal", "internal_style", "\"abc\"", "module_name"),
c295e0f8
XL
946 ];
947 let expected = GENERATED_FILE_COMMENT.to_string()
948 + &[
949 "store.register_group(true, \"clippy::group1\", Some(\"clippy_group1\"), vec![",
950 " LintId::of(module_name::ABC),",
951 " LintId::of(module_name::INTERNAL),",
952 " LintId::of(module_name::SHOULD_ASSERT_EQ),",
953 "])",
954 ]
955 .join("\n")
956 + "\n";
957
958 let result = gen_lint_group_list("group1", lints.iter());
959
960 assert_eq!(expected, result);
961 }
962}