+use crate::clippy_project_root;
use aho_corasick::AhoCorasickBuilder;
-use core::fmt::Write as _;
+use indoc::writedoc;
use itertools::Itertools;
use rustc_lexer::{tokenize, unescape, LiteralKind, TokenKind};
use std::collections::{HashMap, HashSet};
use std::ffi::OsStr;
-use std::fs;
-use std::io::{self, Read as _, Seek as _, Write as _};
+use std::fmt::Write;
+use std::fs::{self, OpenOptions};
+use std::io::{self, Read, Seek, SeekFrom, Write as _};
+use std::ops::Range;
use std::path::{Path, PathBuf};
use walkdir::{DirEntry, WalkDir};
-use crate::clippy_project_root;
-
const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\
// Use that command to update this file and do not edit by hand.\n\
// Manual edits will be overwritten.\n\n";
println!("note: `cargo uitest` still needs to be run to update the test results");
}
+const DEFAULT_DEPRECATION_REASON: &str = "default deprecation note";
+/// Runs the `deprecate` command
+///
+/// This does the following:
+/// * Adds an entry to `deprecated_lints.rs`.
+/// * Removes the lint declaration (and the entire file if applicable)
+///
+/// # Panics
+///
+/// If a file path could not read from or written to
+pub fn deprecate(name: &str, reason: Option<&String>) {
+ fn finish(
+ (lints, mut deprecated_lints, renamed_lints): (Vec<Lint>, Vec<DeprecatedLint>, Vec<RenamedLint>),
+ name: &str,
+ reason: &str,
+ ) {
+ deprecated_lints.push(DeprecatedLint {
+ name: name.to_string(),
+ reason: reason.to_string(),
+ declaration_range: Range::default(),
+ });
+
+ generate_lint_files(UpdateMode::Change, &lints, &deprecated_lints, &renamed_lints);
+ println!("info: `{}` has successfully been deprecated", name);
+
+ if reason == DEFAULT_DEPRECATION_REASON {
+ println!("note: the deprecation reason must be updated in `clippy_lints/src/deprecated_lints.rs`");
+ }
+ println!("note: you must run `cargo uitest` to update the test results");
+ }
+
+ let reason = reason.map_or(DEFAULT_DEPRECATION_REASON, String::as_str);
+ let name_lower = name.to_lowercase();
+ let name_upper = name.to_uppercase();
+
+ let (mut lints, deprecated_lints, renamed_lints) = gather_all();
+ let Some(lint) = lints.iter().find(|l| l.name == name_lower) else { eprintln!("error: failed to find lint `{}`", name); return; };
+
+ let mod_path = {
+ let mut mod_path = PathBuf::from(format!("clippy_lints/src/{}", lint.module));
+ if mod_path.is_dir() {
+ mod_path = mod_path.join("mod");
+ }
+
+ mod_path.set_extension("rs");
+ mod_path
+ };
+
+ let deprecated_lints_path = &*clippy_project_root().join("clippy_lints/src/deprecated_lints.rs");
+
+ if remove_lint_declaration(&name_lower, &mod_path, &mut lints).unwrap_or(false) {
+ declare_deprecated(&name_upper, deprecated_lints_path, reason).unwrap();
+ finish((lints, deprecated_lints, renamed_lints), name, reason);
+ return;
+ }
+
+ eprintln!("error: lint not found");
+}
+
+fn remove_lint_declaration(name: &str, path: &Path, lints: &mut Vec<Lint>) -> io::Result<bool> {
+ fn remove_lint(name: &str, lints: &mut Vec<Lint>) {
+ lints.iter().position(|l| l.name == name).map(|pos| lints.remove(pos));
+ }
+
+ fn remove_test_assets(name: &str) {
+ let test_file_stem = format!("tests/ui/{}", name);
+ let path = Path::new(&test_file_stem);
+
+ // Some lints have their own directories, delete them
+ if path.is_dir() {
+ fs::remove_dir_all(path).ok();
+ return;
+ }
+
+ // Remove all related test files
+ fs::remove_file(path.with_extension("rs")).ok();
+ fs::remove_file(path.with_extension("stderr")).ok();
+ fs::remove_file(path.with_extension("fixed")).ok();
+ }
+
+ fn remove_impl_lint_pass(lint_name_upper: &str, content: &mut String) {
+ let impl_lint_pass_start = content.find("impl_lint_pass!").unwrap_or_else(|| {
+ content
+ .find("declare_lint_pass!")
+ .unwrap_or_else(|| panic!("failed to find `impl_lint_pass`"))
+ });
+ let mut impl_lint_pass_end = content[impl_lint_pass_start..]
+ .find(']')
+ .expect("failed to find `impl_lint_pass` terminator");
+
+ impl_lint_pass_end += impl_lint_pass_start;
+ if let Some(lint_name_pos) = content[impl_lint_pass_start..impl_lint_pass_end].find(&lint_name_upper) {
+ let mut lint_name_end = impl_lint_pass_start + (lint_name_pos + lint_name_upper.len());
+ for c in content[lint_name_end..impl_lint_pass_end].chars() {
+ // Remove trailing whitespace
+ if c == ',' || c.is_whitespace() {
+ lint_name_end += 1;
+ } else {
+ break;
+ }
+ }
+
+ content.replace_range(impl_lint_pass_start + lint_name_pos..lint_name_end, "");
+ }
+ }
+
+ if path.exists() {
+ if let Some(lint) = lints.iter().find(|l| l.name == name) {
+ if lint.module == name {
+ // The lint name is the same as the file, we can just delete the entire file
+ fs::remove_file(path)?;
+ } else {
+ // We can't delete the entire file, just remove the declaration
+
+ if let Some(Some("mod.rs")) = path.file_name().map(OsStr::to_str) {
+ // Remove clippy_lints/src/some_mod/some_lint.rs
+ let mut lint_mod_path = path.to_path_buf();
+ lint_mod_path.set_file_name(name);
+ lint_mod_path.set_extension("rs");
+
+ fs::remove_file(lint_mod_path).ok();
+ }
+
+ let mut content =
+ fs::read_to_string(&path).unwrap_or_else(|_| panic!("failed to read `{}`", path.to_string_lossy()));
+
+ eprintln!(
+ "warn: you will have to manually remove any code related to `{}` from `{}`",
+ name,
+ path.display()
+ );
+
+ assert!(
+ content[lint.declaration_range.clone()].contains(&name.to_uppercase()),
+ "error: `{}` does not contain lint `{}`'s declaration",
+ path.display(),
+ lint.name
+ );
+
+ // Remove lint declaration (declare_clippy_lint!)
+ content.replace_range(lint.declaration_range.clone(), "");
+
+ // Remove the module declaration (mod xyz;)
+ let mod_decl = format!("\nmod {};", name);
+ content = content.replacen(&mod_decl, "", 1);
+
+ remove_impl_lint_pass(&lint.name.to_uppercase(), &mut content);
+ fs::write(path, content).unwrap_or_else(|_| panic!("failed to write to `{}`", path.to_string_lossy()));
+ }
+
+ remove_test_assets(name);
+ remove_lint(name, lints);
+ return Ok(true);
+ }
+ }
+
+ Ok(false)
+}
+
+fn declare_deprecated(name: &str, path: &Path, reason: &str) -> io::Result<()> {
+ let mut file = OpenOptions::new().write(true).open(path)?;
+
+ file.seek(SeekFrom::End(0))?;
+
+ let version = crate::new_lint::get_stabilization_version();
+ let deprecation_reason = if reason == DEFAULT_DEPRECATION_REASON {
+ "TODO"
+ } else {
+ reason
+ };
+
+ writedoc!(
+ file,
+ "
+
+ declare_deprecated_lint! {{
+ /// ### What it does
+ /// Nothing. This lint has been deprecated.
+ ///
+ /// ### Deprecation reason
+ /// {}
+ #[clippy::version = \"{}\"]
+ pub {},
+ \"{}\"
+ }}
+
+ ",
+ deprecation_reason,
+ version,
+ name,
+ reason,
+ )
+}
+
/// Replace substrings if they aren't bordered by identifier characters. Returns `None` if there
/// were no replacements.
fn replace_ident_like(contents: &str, replacements: &[(&str, &str)]) -> Option<String> {
group: String,
desc: String,
module: String,
+ declaration_range: Range<usize>,
}
impl Lint {
#[must_use]
- fn new(name: &str, group: &str, desc: &str, module: &str) -> Self {
+ fn new(name: &str, group: &str, desc: &str, module: &str, declaration_range: Range<usize>) -> Self {
Self {
name: name.to_lowercase(),
group: group.into(),
desc: remove_line_splices(desc),
module: module.into(),
+ declaration_range,
}
}
struct DeprecatedLint {
name: String,
reason: String,
+ declaration_range: Range<usize>,
}
impl DeprecatedLint {
- fn new(name: &str, reason: &str) -> Self {
+ fn new(name: &str, reason: &str, declaration_range: Range<usize>) -> Self {
Self {
name: name.to_lowercase(),
reason: remove_line_splices(reason),
+ declaration_range,
}
}
}
macro_rules! match_tokens {
($iter:ident, $($token:ident $({$($fields:tt)*})? $(($capture:ident))?)*) => {
{
- $($(let $capture =)? if let Some((TokenKind::$token $({$($fields)*})?, _x)) = $iter.next() {
+ $($(let $capture =)? if let Some(LintDeclSearchResult {
+ token_kind: TokenKind::$token $({$($fields)*})?,
+ content: _x,
+ ..
+ }) = $iter.next() {
_x
} else {
continue;
}
}
+struct LintDeclSearchResult<'a> {
+ token_kind: TokenKind,
+ content: &'a str,
+ range: Range<usize>,
+}
+
/// Parse a source file looking for `declare_clippy_lint` macro invocations.
fn parse_contents(contents: &str, module: &str, lints: &mut Vec<Lint>) {
let mut offset = 0usize;
let mut iter = tokenize(contents).map(|t| {
let range = offset..offset + t.len;
offset = range.end;
- (t.kind, &contents[range])
+
+ LintDeclSearchResult {
+ token_kind: t.kind,
+ content: &contents[range.clone()],
+ range,
+ }
});
- while iter.any(|(kind, s)| kind == TokenKind::Ident && s == "declare_clippy_lint") {
+ while let Some(LintDeclSearchResult { range, .. }) = iter.find(
+ |LintDeclSearchResult {
+ token_kind, content, ..
+ }| token_kind == &TokenKind::Ident && *content == "declare_clippy_lint",
+ ) {
+ let start = range.start;
+
let mut iter = iter
.by_ref()
- .filter(|&(kind, _)| !matches!(kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
+ .filter(|t| !matches!(t.token_kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
// matches `!{`
match_tokens!(iter, Bang OpenBrace);
match iter.next() {
// #[clippy::version = "version"] pub
- Some((TokenKind::Pound, _)) => {
+ Some(LintDeclSearchResult {
+ token_kind: TokenKind::Pound,
+ ..
+ }) => {
match_tokens!(iter, OpenBracket Ident Colon Colon Ident Eq Literal{..} CloseBracket Ident);
},
// pub
- Some((TokenKind::Ident, _)) => (),
+ Some(LintDeclSearchResult {
+ token_kind: TokenKind::Ident,
+ ..
+ }) => (),
_ => continue,
}
+
let (name, group, desc) = match_tokens!(
iter,
// LINT_NAME
Ident(name) Comma
// group,
Ident(group) Comma
- // "description" }
- Literal{..}(desc) CloseBrace
+ // "description"
+ Literal{..}(desc)
);
- lints.push(Lint::new(name, group, desc, module));
+
+ if let Some(LintDeclSearchResult {
+ token_kind: TokenKind::CloseBrace,
+ range,
+ ..
+ }) = iter.next()
+ {
+ lints.push(Lint::new(name, group, desc, module, start..range.end));
+ }
}
}
let mut iter = tokenize(contents).map(|t| {
let range = offset..offset + t.len;
offset = range.end;
- (t.kind, &contents[range])
+
+ LintDeclSearchResult {
+ token_kind: t.kind,
+ content: &contents[range.clone()],
+ range,
+ }
});
- while iter.any(|(kind, s)| kind == TokenKind::Ident && s == "declare_deprecated_lint") {
- let mut iter = iter
- .by_ref()
- .filter(|&(kind, _)| !matches!(kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
+
+ while let Some(LintDeclSearchResult { range, .. }) = iter.find(
+ |LintDeclSearchResult {
+ token_kind, content, ..
+ }| token_kind == &TokenKind::Ident && *content == "declare_deprecated_lint",
+ ) {
+ let start = range.start;
+
+ let mut iter = iter.by_ref().filter(|LintDeclSearchResult { ref token_kind, .. }| {
+ !matches!(token_kind, TokenKind::Whitespace | TokenKind::LineComment { .. })
+ });
let (name, reason) = match_tokens!(
iter,
// !{
Ident Ident(name) Comma
// "description"
Literal{kind: LiteralKind::Str{..},..}(reason)
- // }
- CloseBrace
);
- lints.push(DeprecatedLint::new(name, reason));
+
+ if let Some(LintDeclSearchResult {
+ token_kind: TokenKind::CloseBrace,
+ range,
+ ..
+ }) = iter.next()
+ {
+ lints.push(DeprecatedLint::new(name, reason, start..range.end));
+ }
}
}
let mut iter = tokenize(line).map(|t| {
let range = offset..offset + t.len;
offset = range.end;
- (t.kind, &line[range])
+
+ LintDeclSearchResult {
+ token_kind: t.kind,
+ content: &line[range.clone()],
+ range,
+ }
});
+
let (old_name, new_name) = match_tokens!(
iter,
// ("old_name",
"#;
let mut result = Vec::new();
parse_contents(CONTENTS, "module_name", &mut result);
+ for r in &mut result {
+ r.declaration_range = Range::default();
+ }
let expected = vec![
- Lint::new("ptr_arg", "style", "\"really long text\"", "module_name"),
- Lint::new("doc_markdown", "pedantic", "\"single line\"", "module_name"),
+ Lint::new(
+ "ptr_arg",
+ "style",
+ "\"really long text\"",
+ "module_name",
+ Range::default(),
+ ),
+ Lint::new(
+ "doc_markdown",
+ "pedantic",
+ "\"single line\"",
+ "module_name",
+ Range::default(),
+ ),
];
assert_eq!(expected, result);
}
let mut result = Vec::new();
parse_deprecated_contents(DEPRECATED_CONTENTS, &mut result);
+ for r in &mut result {
+ r.declaration_range = Range::default();
+ }
let expected = vec![DeprecatedLint::new(
"should_assert_eq",
"\"`assert!()` will be more flexible with RFC 2011\"",
+ Range::default(),
)];
assert_eq!(expected, result);
}
#[test]
fn test_usable_lints() {
let lints = vec![
- Lint::new("should_assert_eq2", "Not Deprecated", "\"abc\"", "module_name"),
- Lint::new("should_assert_eq2", "internal", "\"abc\"", "module_name"),
- Lint::new("should_assert_eq2", "internal_style", "\"abc\"", "module_name"),
+ Lint::new(
+ "should_assert_eq2",
+ "Not Deprecated",
+ "\"abc\"",
+ "module_name",
+ Range::default(),
+ ),
+ Lint::new(
+ "should_assert_eq2",
+ "internal",
+ "\"abc\"",
+ "module_name",
+ Range::default(),
+ ),
+ Lint::new(
+ "should_assert_eq2",
+ "internal_style",
+ "\"abc\"",
+ "module_name",
+ Range::default(),
+ ),
];
let expected = vec![Lint::new(
"should_assert_eq2",
"Not Deprecated",
"\"abc\"",
"module_name",
+ Range::default(),
)];
assert_eq!(expected, Lint::usable_lints(&lints));
}
#[test]
fn test_by_lint_group() {
let lints = vec![
- Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name"),
- Lint::new("should_assert_eq2", "group2", "\"abc\"", "module_name"),
- Lint::new("incorrect_match", "group1", "\"abc\"", "module_name"),
+ Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name", Range::default()),
+ Lint::new(
+ "should_assert_eq2",
+ "group2",
+ "\"abc\"",
+ "module_name",
+ Range::default(),
+ ),
+ Lint::new("incorrect_match", "group1", "\"abc\"", "module_name", Range::default()),
];
let mut expected: HashMap<String, Vec<Lint>> = HashMap::new();
expected.insert(
"group1".to_string(),
vec![
- Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name"),
- Lint::new("incorrect_match", "group1", "\"abc\"", "module_name"),
+ Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name", Range::default()),
+ Lint::new("incorrect_match", "group1", "\"abc\"", "module_name", Range::default()),
],
);
expected.insert(
"group2".to_string(),
- vec![Lint::new("should_assert_eq2", "group2", "\"abc\"", "module_name")],
+ vec![Lint::new(
+ "should_assert_eq2",
+ "group2",
+ "\"abc\"",
+ "module_name",
+ Range::default(),
+ )],
);
assert_eq!(expected, Lint::by_lint_group(lints.into_iter()));
}
#[test]
fn test_gen_deprecated() {
let lints = vec![
- DeprecatedLint::new("should_assert_eq", "\"has been superseded by should_assert_eq2\""),
- DeprecatedLint::new("another_deprecated", "\"will be removed\""),
+ DeprecatedLint::new(
+ "should_assert_eq",
+ "\"has been superseded by should_assert_eq2\"",
+ Range::default(),
+ ),
+ DeprecatedLint::new("another_deprecated", "\"will be removed\"", Range::default()),
];
let expected = GENERATED_FILE_COMMENT.to_string()
#[test]
fn test_gen_lint_group_list() {
let lints = vec![
- Lint::new("abc", "group1", "\"abc\"", "module_name"),
- Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name"),
- Lint::new("internal", "internal_style", "\"abc\"", "module_name"),
+ Lint::new("abc", "group1", "\"abc\"", "module_name", Range::default()),
+ Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name", Range::default()),
+ Lint::new("internal", "internal_style", "\"abc\"", "module_name", Range::default()),
];
let expected = GENERATED_FILE_COMMENT.to_string()
+ &[