use super::*;
pub(crate) static MANUAL_IS_EMPTY_REGEX: OnceLock<Regex> = OnceLock::new();
pub(crate) static MANUAL_CONTAINS_REGEX: OnceLock<Regex> = OnceLock::new();
pub(crate) static MANUAL_STRIP_PREFIX_REGEX: OnceLock<Regex> = OnceLock::new();
pub(crate) static MANUAL_UNWRAP_OR_DEFAULT_REGEX: OnceLock<Regex> = OnceLock::new();
pub(crate) static QUESTION_MARK_MATCH_OK_FIRST_REGEX: OnceLock<Regex> = OnceLock::new();
pub(crate) static QUESTION_MARK_MATCH_ERR_FIRST_REGEX: OnceLock<Regex> = OnceLock::new();
pub(crate) static QUESTION_MARK_IF_LET_REGEX: OnceLock<Regex> = OnceLock::new();
pub(crate) fn analyse_modernisation_rules(
file: &SourceFile,
source: &str,
findings: &mut Vec<Finding>,
) {
let searchable = strip_rust_comments_after_string_mask(&strip_rust_string_literals(source));
analyse_manual_is_empty(file, &searchable, findings);
analyse_manual_contains(file, &searchable, findings);
analyse_manual_strip_prefix(file, &searchable, findings);
analyse_manual_unwrap_or_default(file, &searchable, findings);
analyse_question_mark_candidate(file, &searchable, findings);
}
pub(crate) fn analyse_manual_is_empty(
file: &SourceFile,
searchable: &str,
findings: &mut Vec<Finding>,
) {
let regex = static_regex(
&MANUAL_IS_EMPTY_REGEX,
r"(?:\.len\s*\(\s*\)\s*(?:==|!=)\s*0\b|\b0\s*(?:==|!=)\s*[A-Za-z_][\w\.]*\.len\s*\(\s*\))",
);
for (line_index, line) in searchable.lines().enumerate() {
if regex.is_match(line) {
push_modernisation_finding(
file,
line_index + 1,
"modernisation.manual-is-empty",
"Use `.is_empty()` instead of comparing `.len()` against zero.",
findings,
);
}
}
}
pub(crate) fn analyse_manual_contains(
file: &SourceFile,
searchable: &str,
findings: &mut Vec<Finding>,
) {
let regex = static_regex(
&MANUAL_CONTAINS_REGEX,
r"\.iter\s*\(\s*\)\s*\.any\s*\(\s*\|\s*(?P<arg>[A-Za-z_][A-Za-z0-9_]*)\s*\|\s*(?:\*\s*(?P<deref>[A-Za-z_][A-Za-z0-9_]*)\s*==\s*[A-Za-z_][\w\.]*|(?P<noderef>[A-Za-z_][A-Za-z0-9_]*)\s*==\s*&\s*[A-Za-z_][\w\.]*)\s*\)",
);
let check = ModernisationCheck {
compiled: regex,
is_valid: manual_contains_match_is_valid,
rule_id: "modernisation.manual-contains",
message: "Use `.contains(&value)` instead of `.iter().any(|x| x == &value)`.",
};
scan_modernisation_matches(&check, searchable, file, findings);
}
fn manual_contains_match_is_valid(captures: ®ex::Captures<'_>) -> bool {
let arg = capture_named(captures, "arg");
let lhs = match capture_named(captures, "deref") {
"" => capture_named(captures, "noderef"),
deref => deref,
};
!arg.is_empty() && arg == lhs
}
pub(crate) fn analyse_manual_strip_prefix(
file: &SourceFile,
searchable: &str,
findings: &mut Vec<Finding>,
) {
let regex = static_regex(
&MANUAL_STRIP_PREFIX_REGEX,
r"if\s+(?P<first>[A-Za-z_][A-Za-z0-9_]*)\s*\.starts_with\s*\(\s*(?P<second>[A-Za-z_][A-Za-z0-9_]*)\s*\)\s*\{\s*&\s*(?P<first_again>[A-Za-z_][A-Za-z0-9_]*)\s*\[\s*(?P<second_again>[A-Za-z_][A-Za-z0-9_]*)\s*\.len\s*\(\s*\)\s*\.\.\s*\]",
);
let check = ModernisationCheck {
compiled: regex,
is_valid: manual_strip_prefix_match_is_valid,
rule_id: "modernisation.manual-strip-prefix",
message: "Use `strip_prefix` instead of slicing after `starts_with`.",
};
scan_modernisation_matches(&check, searchable, file, findings);
}
fn manual_strip_prefix_match_is_valid(captures: ®ex::Captures<'_>) -> bool {
let first = capture_named(captures, "first");
let second = capture_named(captures, "second");
!first.is_empty()
&& !second.is_empty()
&& first == capture_named(captures, "first_again")
&& second == capture_named(captures, "second_again")
}
pub(crate) fn analyse_manual_unwrap_or_default(
file: &SourceFile,
searchable: &str,
findings: &mut Vec<Finding>,
) {
let regex = static_regex(
&MANUAL_UNWRAP_OR_DEFAULT_REGEX,
r"match\s+[A-Za-z_][\w\.]*\s*\{\s*Some\s*\(\s*(?P<arg>[A-Za-z_][A-Za-z0-9_]*)\s*\)\s*=>\s*(?P<ret>[A-Za-z_][A-Za-z0-9_]*)\s*,\s*None\s*=>\s*(?:Default\s*::\s*default\s*\(\s*\)|String\s*::\s*new\s*\(\s*\)|Vec\s*::\s*new\s*\(\s*\)|0|0_[a-z0-9]+)\s*,?\s*\}",
);
let check = ModernisationCheck {
compiled: regex,
is_valid: manual_unwrap_or_default_match_is_valid,
rule_id: "modernisation.manual-unwrap-or-default",
message:
"Use `.unwrap_or_default()` instead of a manual `match` with `Default::default()`.",
};
scan_modernisation_matches(&check, searchable, file, findings);
}
fn manual_unwrap_or_default_match_is_valid(captures: ®ex::Captures<'_>) -> bool {
let arg = capture_named(captures, "arg");
!arg.is_empty() && arg == capture_named(captures, "ret")
}
pub(crate) fn analyse_question_mark_candidate(
file: &SourceFile,
searchable: &str,
findings: &mut Vec<Finding>,
) {
for check in question_mark_checks() {
scan_question_mark(check, searchable, file, findings);
}
}
fn question_mark_checks() -> [QuestionMarkCheck; 3] {
[
QuestionMarkCheck {
compiled: question_mark_ok_first_regex(),
is_valid: is_match_arm_balanced,
},
QuestionMarkCheck {
compiled: question_mark_err_first_regex(),
is_valid: is_match_arm_balanced,
},
QuestionMarkCheck {
compiled: question_mark_if_let_regex(),
is_valid: is_if_let_balanced,
},
]
}
fn question_mark_ok_first_regex() -> &'static Regex {
static_regex(
&QUESTION_MARK_MATCH_OK_FIRST_REGEX,
r"match\s+[A-Za-z_][\w\.()]*\s*\{\s*Ok\s*\(\s*(?P<ok_a>[A-Za-z_]\w*)\s*\)\s*=>\s*(?P<ok_b>[A-Za-z_]\w*)\s*,\s*Err\s*\(\s*(?P<err_a>[A-Za-z_]\w*)\s*\)\s*=>\s*return\s+Err\s*\(\s*(?P<err_b>[A-Za-z_]\w*)\s*\)\s*,?\s*\}",
)
}
fn question_mark_err_first_regex() -> &'static Regex {
static_regex(
&QUESTION_MARK_MATCH_ERR_FIRST_REGEX,
r"match\s+[A-Za-z_][\w\.()]*\s*\{\s*Err\s*\(\s*(?P<err_a>[A-Za-z_]\w*)\s*\)\s*=>\s*return\s+Err\s*\(\s*(?P<err_b>[A-Za-z_]\w*)\s*\)\s*,\s*Ok\s*\(\s*(?P<ok_a>[A-Za-z_]\w*)\s*\)\s*=>\s*(?P<ok_b>[A-Za-z_]\w*)\s*,?\s*\}",
)
}
fn question_mark_if_let_regex() -> &'static Regex {
static_regex(
&QUESTION_MARK_IF_LET_REGEX,
r"if\s+let\s+Err\s*\(\s*(?P<e>[A-Za-z_]\w*)\s*\)\s*=\s*[^{;]+?\s*\{\s*return\s+Err\s*\(\s*(?P<e2>[A-Za-z_]\w*)\s*\)\s*;?\s*\}",
)
}
struct QuestionMarkCheck {
compiled: &'static Regex,
is_valid: fn(®ex::Captures<'_>) -> bool,
}
fn is_match_arm_balanced(captures: ®ex::Captures<'_>) -> bool {
let ok_a = capture_named(captures, "ok_a");
let err_a = capture_named(captures, "err_a");
!ok_a.is_empty()
&& !err_a.is_empty()
&& ok_a == capture_named(captures, "ok_b")
&& err_a == capture_named(captures, "err_b")
}
fn is_if_let_balanced(captures: ®ex::Captures<'_>) -> bool {
let first = capture_named(captures, "e");
!first.is_empty() && first == capture_named(captures, "e2")
}
fn scan_question_mark(
check: QuestionMarkCheck,
searchable: &str,
file: &SourceFile,
findings: &mut Vec<Finding>,
) {
let starts = line_starts(searchable);
for captures in check.compiled.captures_iter(searchable) {
if !(check.is_valid)(&captures) {
continue;
}
let Some(full) = captures.get(0) else {
continue;
};
let line = byte_line_from_starts(&starts, full.start());
push_modernisation_finding(
file,
line,
"modernisation.question-mark-candidate",
"Use `?` instead of a manual `match` / `if let Err` that returns the same error.",
findings,
);
}
}
struct ModernisationCheck {
compiled: &'static Regex,
is_valid: fn(®ex::Captures<'_>) -> bool,
rule_id: &'static str,
message: &'static str,
}
fn scan_modernisation_matches(
check: &ModernisationCheck,
searchable: &str,
file: &SourceFile,
findings: &mut Vec<Finding>,
) {
let starts = line_starts(searchable);
for captures in check.compiled.captures_iter(searchable) {
if !(check.is_valid)(&captures) {
continue;
}
let Some(full) = captures.get(0) else {
continue;
};
let line = byte_line_from_starts(&starts, full.start());
push_modernisation_finding(file, line, check.rule_id, check.message, findings);
}
}
fn capture_named<'a>(captures: ®ex::Captures<'a>, name: &str) -> &'a str {
captures.name(name).map(|m| m.as_str()).unwrap_or("")
}
fn push_modernisation_finding(
file: &SourceFile,
line: usize,
rule_id: &'static str,
message: &'static str,
findings: &mut Vec<Finding>,
) {
findings.push(finding(SimpleFindingDescriptor {
rule_id,
message: message.into(),
file,
line: Some(line),
severity: Severity::Advisory,
pillar: Pillar::Modernisation,
}));
}