]> git.proxmox.com Git - rustc.git/blob - 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
1 use aho_corasick::AhoCorasickBuilder;
2 use core::fmt::Write as _;
3 use itertools::Itertools;
4 use rustc_lexer::{tokenize, unescape, LiteralKind, TokenKind};
5 use std::collections::{HashMap, HashSet};
6 use std::ffi::OsStr;
7 use std::fs;
8 use std::io::{self, Read as _, Seek as _, Write as _};
9 use std::path::{Path, PathBuf};
10 use walkdir::{DirEntry, WalkDir};
11
12 use crate::clippy_project_root;
13
14 const 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
18 const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html";
19
20 #[derive(Clone, Copy, PartialEq, Eq)]
21 pub enum UpdateMode {
22 Check,
23 Change,
24 }
25
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
35 pub 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 }
39
40 fn 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);
48 let mut sorted_usable_lints = usable_lints.clone();
49 sorted_usable_lints.sort_by_key(|lint| lint.name.clone());
50
51 replace_region_in_file(
52 update_mode,
53 Path::new("README.md"),
54 "[There are over ",
55 " lints included in this crate!]",
56 |res| {
57 write!(res, "{}", round_to_fifty(usable_lints.len())).unwrap();
58 },
59 );
60
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
71 replace_region_in_file(
72 update_mode,
73 Path::new("CHANGELOG.md"),
74 "<!-- begin autogenerated links to lint list -->\n",
75 "<!-- end autogenerated links to lint list -->",
76 |res| {
77 for lint in usable_lints
78 .iter()
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 )
86 .sorted()
87 {
88 writeln!(res, "[`{}`]: {}#{}", lint, DOCS_LINK, lint).unwrap();
89 }
90 },
91 );
92
93 // This has to be in lib.rs, otherwise rustfmt doesn't work
94 replace_region_in_file(
95 update_mode,
96 Path::new("clippy_lints/src/lib.rs"),
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 );
105
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,
114 &gen_deprecated(deprecated_lints),
115 );
116
117 let all_group_lints = usable_lints.iter().filter(|l| {
118 matches!(
119 &*l.group,
120 "correctness" | "suspicious" | "style" | "complexity" | "perf"
121 )
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);
125
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,
132 );
133 }
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);
140 }
141
142 pub fn print_lints() {
143 let (lint_list, _, _) = gather_all();
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 {
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
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)]
177 pub 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.
331 fn 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
364 fn round_to_fifty(count: usize) -> usize {
365 count / 50 * 50
366 }
367
368 fn 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
381 fn 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.
390 #[derive(Clone, PartialEq, Eq, Debug)]
391 struct Lint {
392 name: String,
393 group: String,
394 desc: String,
395 module: String,
396 }
397
398 impl Lint {
399 #[must_use]
400 fn new(name: &str, group: &str, desc: &str, module: &str) -> Self {
401 Self {
402 name: name.to_lowercase(),
403 group: group.into(),
404 desc: remove_line_splices(desc),
405 module: module.into(),
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()
414 .filter(|l| !l.group.starts_with("internal"))
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
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
432 #[derive(Clone, PartialEq, Eq, Debug)]
433 struct DeprecatedLint {
434 name: String,
435 reason: String,
436 }
437 impl 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
446 struct RenamedLint {
447 old_name: String,
448 new_name: String,
449 }
450 impl 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
459 /// Generates the code for registering a group
460 fn 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
466 let _ = writeln!(
467 output,
468 "store.register_group(true, \"clippy::{0}\", Some(\"clippy_{0}\"), vec![",
469 group_name
470 );
471 for (module, name) in details {
472 let _ = writeln!(output, " LintId::of({}::{}),", module, name);
473 }
474 output.push_str("])\n");
475
476 output
477 }
478
479 /// Generates the `register_removed` code
480 #[must_use]
481 fn gen_deprecated(lints: &[DeprecatedLint]) -> String {
482 let mut output = GENERATED_FILE_COMMENT.to_string();
483 output.push_str("{\n");
484 for lint in lints {
485 let _ = write!(
486 output,
487 concat!(
488 " store.register_removed(\n",
489 " \"clippy::{}\",\n",
490 " \"{}\",\n",
491 " );\n"
492 ),
493 lint.name, lint.reason,
494 );
495 }
496 output.push_str("}\n");
497
498 output
499 }
500
501 /// Generates the code for registering lints
502 #[must_use]
503 fn 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 {
518 output.push_str(" #[cfg(feature = \"internal\")]\n");
519 }
520 let _ = writeln!(output, " {}::{},", module_name, lint_name);
521 }
522 output.push_str("])\n");
523
524 output
525 }
526
527 fn 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
534 }
535
536 fn 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 }
550 }
551 res.push_str("\nfn main() {}\n");
552 res
553 }
554
555 fn 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";
560
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
567 }
568
569 /// Gathers all lints defined in `clippy_lints/src`
570 fn 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)
600 }
601
602 fn 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)
606 .filter(|f| f.path().extension() == Some(OsStr::new("rs")))
607 .map(move |f| (f.path().strip_prefix(&root_path).unwrap().to_path_buf(), f))
608 }
609
610 macro_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.
625 fn 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.
662 fn 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
690 fn 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 }
707 }
708
709 /// Removes the line splices and surrounding quotes from a string literal
710 fn 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
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
732 fn replace_region_in_file(
733 update_mode: UpdateMode,
734 path: &Path,
735 start: &str,
736 end: &str,
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);
751 }
752 },
753 }
754 }
755
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.
758 fn 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)
775 }
776
777 fn 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 }
794 }
795
796 #[allow(clippy::needless_pass_by_value)]
797 fn panic_file(error: io::Error, name: &Path, action: &str) -> ! {
798 panic!("failed to {} file `{}`: {}", action, name.display(), error)
799 }
800
801 fn 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 }
817 }
818
819 fn write_file(path: &Path, contents: &str) {
820 fs::write(path, contents).unwrap_or_else(|e| panic_file(e, path, "write"));
821 }
822
823 #[cfg(test)]
824 mod tests {
825 use super::*;
826
827 #[test]
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 }
837
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 ];
852 assert_eq!(expected, result);
853 }
854
855 #[test]
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 )];
873 assert_eq!(expected, result);
874 }
875
876 #[test]
877 fn test_usable_lints() {
878 let lints = vec![
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"),
882 ];
883 let expected = vec![Lint::new(
884 "should_assert_eq2",
885 "Not Deprecated",
886 "\"abc\"",
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![
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"),
898 ];
899 let mut expected: HashMap<String, Vec<Lint>> = HashMap::new();
900 expected.insert(
901 "group1".to_string(),
902 vec![
903 Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name"),
904 Lint::new("incorrect_match", "group1", "\"abc\"", "module_name"),
905 ],
906 );
907 expected.insert(
908 "group2".to_string(),
909 vec![Lint::new("should_assert_eq2", "group2", "\"abc\"", "module_name")],
910 );
911 assert_eq!(expected, Lint::by_lint_group(lints.into_iter()));
912 }
913
914 #[test]
915 fn test_gen_deprecated() {
916 let lints = vec![
917 DeprecatedLint::new("should_assert_eq", "\"has been superseded by should_assert_eq2\""),
918 DeprecatedLint::new("another_deprecated", "\"will be removed\""),
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
937 assert_eq!(expected, gen_deprecated(&lints));
938 }
939
940 #[test]
941 fn test_gen_lint_group_list() {
942 let lints = vec![
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"),
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 }