]>
Commit | Line | Data |
---|---|---|
04454e1e FG |
1 | use aho_corasick::AhoCorasickBuilder; |
2 | use core::fmt::Write as _; | |
c295e0f8 | 3 | use itertools::Itertools; |
04454e1e FG |
4 | use rustc_lexer::{tokenize, unescape, LiteralKind, TokenKind}; |
5 | use std::collections::{HashMap, HashSet}; | |
c295e0f8 XL |
6 | use std::ffi::OsStr; |
7 | use std::fs; | |
04454e1e FG |
8 | use std::io::{self, Read as _, Seek as _, Write as _}; |
9 | use std::path::{Path, PathBuf}; | |
10 | use walkdir::{DirEntry, WalkDir}; | |
c295e0f8 XL |
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 | ||
04454e1e | 18 | const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html"; |
f20569fa | 19 | |
923072b8 | 20 | #[derive(Clone, Copy, PartialEq, Eq)] |
f20569fa XL |
21 | pub 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 |
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 | } | |
f20569fa | 39 | |
04454e1e FG |
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); | |
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 | ||
142 | pub 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)] | |
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 | ||
f20569fa XL |
364 | fn round_to_fifty(count: usize) -> usize { |
365 | count / 50 * 50 | |
366 | } | |
c295e0f8 XL |
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. | |
923072b8 | 390 | #[derive(Clone, PartialEq, Eq, Debug)] |
c295e0f8 XL |
391 | struct Lint { |
392 | name: String, | |
393 | group: String, | |
394 | desc: String, | |
c295e0f8 XL |
395 | module: String, |
396 | } | |
397 | ||
398 | impl 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 |
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 | ||
c295e0f8 XL |
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 | ||
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 | 481 | fn 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] | |
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 { | |
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 |
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 | |
c295e0f8 XL |
534 | } |
535 | ||
04454e1e FG |
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 | } | |
c295e0f8 | 550 | } |
04454e1e FG |
551 | res.push_str("\nfn main() {}\n"); |
552 | res | |
553 | } | |
c295e0f8 | 554 | |
04454e1e FG |
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"; | |
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` |
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) | |
c295e0f8 XL |
600 | } |
601 | ||
04454e1e FG |
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) | |
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 | ||
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 | } | |
c295e0f8 XL |
707 | } |
708 | ||
04454e1e FG |
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 | |
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 |
732 | fn 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. | |
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) | |
c295e0f8 XL |
775 | } |
776 | ||
04454e1e FG |
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 | } | |
c295e0f8 XL |
794 | } |
795 | ||
04454e1e FG |
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) | |
c295e0f8 XL |
799 | } |
800 | ||
04454e1e FG |
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 | } | |
c295e0f8 | 817 | } |
c295e0f8 | 818 | |
04454e1e FG |
819 | fn 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)] | |
824 | mod 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 | } |