code-buster 0.11.0

Repository-aware static analysis for AI-assisted development
Documentation
fn cpp_functions(
    path: &str,
    source: &str,
    stringifying_macros: &BTreeSet<String>,
) -> Vec<FunctionSource> {
    let masked = mask_comments_and_strings(source);
    let raw_lines = masked.split('\n').collect::<Vec<_>>();
    let preprocessed = mask_inactive_preprocessor_branches(&raw_lines);
    let lines = mask_cpp_stringifying_arguments(preprocessed, stringifying_macros);
    let mut functions = Vec::new();
    let mut index = 0;
    while index < lines.len() {
        let Some(name) = discover_cpp_function_candidate(&lines, index) else {
            index += 1;
            continue;
        };
        let end = extract_cpp_function(path, &lines, index, name, &mut functions);
        index = end + 1;
    }
    functions
}

fn discover_cpp_function_candidate(lines: &[String], index: usize) -> Option<String> {
    let first_line = &lines[index];
    if !qualifies_as_cpp_declaration(first_line) {
        return None;
    }

    let (matched, name) = cpp_declaration_candidate(first_line);
    if matched {
        return name;
    }
    if !is_cpp_signature_start(first_line) {
        return None;
    }

    cpp_declaration_candidate(&assemble_cpp_signature(lines, index)).1
}

fn qualifies_as_cpp_declaration(line: &str) -> bool {
    let trimmed = line.trim_start();
    !CPP_CONTROL.is_match(trimmed)
        && !trimmed.starts_with(':')
        && !trimmed.starts_with("typedef ")
}

fn cpp_declaration_candidate(declaration: &str) -> (bool, Option<String>) {
    let Some(declaration) = CPP_OBJECTIVE_C_METHOD
        .captures(declaration)
        .or_else(|| CPP_FUNCTION.captures(declaration))
    else {
        return (false, None);
    };
    let name = declaration.name("name").map(|name| name.as_str());
    (
        true,
        name.filter(|name| !CPP_CONTROL_NAME.is_match(name))
            .map(str::to_owned),
    )
}

fn is_cpp_signature_start(line: &str) -> bool {
    CPP_UNQUALIFIED_SIGNATURE.is_match(line)
        || CPP_QUALIFIED_SIGNATURE.is_match(line)
        || CPP_OBJECTIVE_C_SIGNATURE.is_match(line)
}

fn assemble_cpp_signature(lines: &[String], index: usize) -> String {
    let mut signature = String::new();
    for line in lines.iter().skip(index).take(21) {
        signature.push_str(line);
        signature.push('\n');
        if line.contains('{') || line.contains(';') {
            break;
        }
    }
    signature
}

fn extract_cpp_function(
    path: &str,
    lines: &[String],
    start: usize,
    name: String,
    functions: &mut Vec<FunctionSource>,
) -> usize {
    let end = brace_delimited_end_owned(lines, start);
    let source = lines[start..=end].join("\n");
    functions.extend(cpp_function_and_lambdas(
        path,
        name,
        start + 1,
        &source,
    ));
    end
}

fn cpp_function_and_lambdas(
    path: &str,
    name: String,
    line: usize,
    source: &str,
) -> Vec<FunctionSource> {
    let masked = mask_comments_and_strings(source);
    let mut lambdas = BTreeMap::<usize, (usize, usize)>::new();
    for matched in CPP_LAMBDA.find_iter(&masked) {
        let Some(relative_opening) = masked[matched.start()..matched.end()].find('{') else {
            continue;
        };
        let opening = matched.start() + relative_opening;
        let Some(end) = matching_delimiter(&masked, opening, b'{', b'}') else {
            continue;
        };
        lambdas.insert(opening, (matched.start(), end));
    }

    let mut functions = vec![FunctionSource {
        path: path.to_owned(),
        name,
        line,
        end_line: line + source.matches('\n').count(),
        contract_signature: false,
        source: mask_nested_cpp_lambdas(source, 0, None, source.len() - 1, &lambdas),
    }];
    for (opening, (start, end)) in &lambdas {
        functions.push(FunctionSource {
            path: path.to_owned(),
            name: "<lambda>".to_owned(),
            line: line + source[..*start].matches('\n').count(),
            end_line: line + source[..=*end].matches('\n').count(),
            contract_signature: false,
            source: mask_nested_cpp_lambdas(source, *start, Some(*opening), *end, &lambdas),
        });
    }
    functions
}

fn mask_nested_cpp_lambdas(
    source: &str,
    start: usize,
    opening: Option<usize>,
    end: usize,
    lambdas: &BTreeMap<usize, (usize, usize)>,
) -> String {
    let mut result = source.as_bytes()[start..=end].to_vec();
    for (nested_opening, (nested_start, nested_end)) in lambdas {
        if opening.is_some_and(|opening| *nested_opening <= opening) || *nested_end > end {
            continue;
        }
        for byte in &mut result[nested_start - start..=nested_end - start] {
            if !matches!(*byte, b'\n' | b'\r') {
                *byte = b' ';
            }
        }
    }
    String::from_utf8(result).expect("C++ lambda masking preserves UTF-8")
}

// code-buster-ignore complex-function: macro graph expansion keeps definitions and recursive stringification state together.
fn cpp_stringifying_macros<'a>(sources: impl Iterator<Item = &'a str>) -> BTreeSet<String> {
    let mut definitions = BTreeMap::<String, (String, String)>::new();
    for source in sources {
        for captures in CPP_FUNCTION_MACRO_DEFINITION.captures_iter(source) {
            definitions.insert(
                captures.get(1).unwrap().as_str().to_owned(),
                (
                    captures.get(2).unwrap().as_str().to_owned(),
                    captures.get(3).unwrap().as_str().to_owned(),
                ),
            );
        }
    }
    let mut result = BTreeSet::new();
    for (name, (parameter, replacement)) in &definitions {
        let pattern = Regex::new(&format!(r"(^|[^#])#\s*{}\b", regex::escape(parameter))).unwrap();
        if pattern.is_match(replacement) {
            result.insert(name.clone());
        }
    }
    loop {
        let mut changed = false;
        for (name, (parameter, replacement)) in &definitions {
            if result.contains(name) {
                continue;
            }
            let wraps_stringifier = result.iter().any(|stringifier| {
                Regex::new(&format!(
                    r"\b{}\s*\(\s*{}\s*\)",
                    regex::escape(stringifier),
                    regex::escape(parameter)
                ))
                .unwrap()
                .is_match(replacement)
            });
            if wraps_stringifier {
                result.insert(name.clone());
                changed = true;
            }
        }
        if !changed {
            break;
        }
    }
    result
}

fn mask_cpp_stringifying_arguments(
    lines: Vec<String>,
    macro_names: &BTreeSet<String>,
) -> Vec<String> {
    if macro_names.is_empty() {
        return lines;
    }
    let source = lines.join("\n");
    let mut masked = source.as_bytes().to_vec();
    let Ok(invocation) = Regex::new(&format!(
        r"\b(?:{})\s*\(",
        macro_names
            .iter()
            .map(|name| regex::escape(name))
            .collect::<Vec<_>>()
            .join("|")
    )) else {
        return lines;
    };
    for matched in invocation.find_iter(&source) {
        let line_start = source[..matched.start()]
            .rfind('\n')
            .map_or(0, |index| index + 1);
        if CPP_DEFINE.is_match(&source[line_start..matched.start()]) {
            continue;
        }
        let Some(open) = source[matched.start()..]
            .find('(')
            .map(|offset| matched.start() + offset)
        else {
            continue;
        };
        let Some(close) = matching_delimiter(&source, open, b'(', b')') else {
            continue;
        };
        for byte in &mut masked[open + 1..close] {
            if !matches!(*byte, b'\n' | b'\r') {
                *byte = b' ';
            }
        }
    }
    String::from_utf8(masked)
        .expect("C++ macro masking preserves UTF-8")
        .split('\n')
        .map(str::to_owned)
        .collect()
}

fn brace_delimited_end_owned(lines: &[String], start: usize) -> usize {
    let mut depth = 0isize;
    let mut started = false;
    for (index, line) in lines.iter().enumerate().skip(start) {
        depth += line.matches('{').count() as isize;
        depth -= line.matches('}').count() as isize;
        started |= line.contains('{');
        if started && depth <= 0 {
            return index;
        }
    }
    lines.len().saturating_sub(1)
}
static CPP_LAMBDA: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(
        r"\[[^\]\r\n]*\]\s*(?:\([^;{}]*\))?\s*(?:(?:mutable|constexpr|consteval)\s+)*(?:noexcept(?:\s*\([^)]*\))?\s*)?(?:->\s*[^{}]+)?\{",
    )
    .expect("C++ lambda regex must compile")
});
static CPP_FUNCTION_MACRO_DEFINITION: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"(?m)^\s*#\s*define\s+([A-Za-z_]\w*)\s*\(\s*([A-Za-z_]\w*)\s*\)\s+([^\r\n]+)$")
        .expect("C++ macro definition regex must compile")
});
static CPP_DEFINE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"^\s*#\s*define\b").expect("C++ define regex must compile"));
static CPP_FUNCTION: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(
        r"(?:^|[\s*&])(?:[A-Za-z_]\w*::)*(?P<name>[A-Za-z_]\w*)\s*\([^;{}]*\)\s*(?:const\s*)?\{",
    )
    .expect("C++ function regex must compile")
});
static CPP_UNQUALIFIED_SIGNATURE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"^\s*(?:[A-Za-z_]\w*[\s*&]+)+[A-Za-z_]\w*\s*\(")
        .expect("C++ unqualified signature regex must compile")
});
static CPP_QUALIFIED_SIGNATURE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"^\s*(?:[A-Za-z_]\w*::)+(?:~?[A-Za-z_]\w*)\s*\(")
        .expect("C++ qualified signature regex must compile")
});
static CPP_OBJECTIVE_C_SIGNATURE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"^\s*[-+]\s*\(").expect("Objective-C signature regex must compile")
});
static CPP_OBJECTIVE_C_METHOD: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"^\s*[-+]\s*\([^)]*\)\s*(?P<name>[A-Za-z_]\w*)[^;{}]*\{")
        .expect("Objective-C method regex must compile")
});
static CPP_CONTROL_NAME: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"^(?:if|for|while|switch|catch)$").expect("C++ control name regex must compile")
});
static CPP_CONTROL: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"^(?:if|for|while|switch|catch)\b").expect("C++ control regex must compile")
});