ripr 0.10.0

Find static mutation-exposure gaps before expensive mutation testing
Documentation
pub(super) fn equality_assertion_arguments(line: &str) -> Option<Vec<String>> {
    ["assert_eq!", "assert_ne!"]
        .iter()
        .find_map(|macro_name| macro_invocation_arguments(line, macro_name))
}

pub(super) fn custom_assertion_arguments(line: &str) -> Option<Vec<String>> {
    let open = line.find('(')?;
    delimited_contents_at(line, open).map(|contents| split_top_level_commas(&contents))
}

fn macro_invocation_arguments(line: &str, macro_name: &str) -> Option<Vec<String>> {
    line.match_indices(macro_name)
        .filter_map(|(index, _)| {
            let prefix_ok = index == 0
                || !line[..index]
                    .chars()
                    .next_back()
                    .is_some_and(|ch| ch.is_ascii_alphanumeric() || ch == '_');
            let suffix_start = index + macro_name.len();
            let open_offset = line[suffix_start..]
                .char_indices()
                .find_map(|(offset, ch)| (!ch.is_whitespace()).then_some((offset, ch)))?;
            if !prefix_ok || open_offset.1 != '(' {
                return None;
            }
            let open = suffix_start + open_offset.0;
            delimited_contents_at(line, open).map(|contents| split_top_level_commas(&contents))
        })
        .next()
}

fn delimited_contents_at(text: &str, open_index: usize) -> Option<String> {
    let open = text.as_bytes().get(open_index).copied()?;
    if open != b'(' {
        return None;
    }
    let mut depth = 0i32;
    let mut in_string = false;
    let mut escaped = false;
    let mut content_start = None;
    for (offset, ch) in text[open_index..].char_indices() {
        let index = open_index + offset;
        if in_string {
            if escaped {
                escaped = false;
            } else if ch == '\\' {
                escaped = true;
            } else if ch == '"' {
                in_string = false;
            }
            continue;
        }
        match ch {
            '"' => in_string = true,
            '(' => {
                depth += 1;
                if depth == 1 {
                    content_start = Some(index + ch.len_utf8());
                }
            }
            ')' => {
                depth -= 1;
                if depth == 0 {
                    let start = content_start?;
                    return Some(text[start..index].to_string());
                }
            }
            _ => {}
        }
    }
    None
}

fn split_top_level_commas(text: &str) -> Vec<String> {
    let mut args = Vec::new();
    let mut start = 0usize;
    let mut paren_depth = 0i32;
    let mut bracket_depth = 0i32;
    let mut brace_depth = 0i32;
    let mut in_string = false;
    let mut escaped = false;
    for (index, ch) in text.char_indices() {
        if in_string {
            if escaped {
                escaped = false;
            } else if ch == '\\' {
                escaped = true;
            } else if ch == '"' {
                in_string = false;
            }
            continue;
        }
        match ch {
            '"' => in_string = true,
            '(' => paren_depth += 1,
            ')' => paren_depth -= 1,
            '[' => bracket_depth += 1,
            ']' => bracket_depth -= 1,
            '{' => brace_depth += 1,
            '}' => brace_depth -= 1,
            ',' if paren_depth == 0 && bracket_depth == 0 && brace_depth == 0 => {
                args.push(text[start..index].trim().to_string());
                start = index + ch.len_utf8();
            }
            _ => {}
        }
    }
    let tail = text[start..].trim();
    if !tail.is_empty() {
        args.push(tail.to_string());
    }
    args
}

pub(super) fn comparable_expression(expression: &str) -> String {
    expression
        .split_whitespace()
        .collect::<String>()
        .trim_start_matches('&')
        .to_string()
}