code-buster 0.11.0

Repository-aware static analysis for AI-assisted development
Documentation
use crate::findings::{Finding, Severity};
use crate::rule_catalog;
use crate::source_analysis_context::SourceAnalysisContext;

pub fn analyze(context: &SourceAnalysisContext<'_>) -> Vec<Finding> {
    let mut findings = Vec::new();
    for (path, source) in context
        .sources()
        .iter()
        .filter(|(path, _)| path.ends_with(".odin"))
    {
        let code = odin_code(source);
        for (index, line) in code.split('\n').enumerate() {
            let line_number = index + 1;
            if contains_identifier_call(line, "panic") {
                findings.extend(finding(
                    "odin-panic-call",
                    path,
                    line_number,
                    "panic call can terminate the process",
                ));
            }
            if contains_identifier_call(line, "transmute") {
                findings.extend(finding(
                    "odin-transmute",
                    path,
                    line_number,
                    "transmute requires a representation invariant",
                ));
            }
            if contains_identifier(line, "rawptr") {
                findings.extend(finding(
                    "odin-raw-pointer",
                    path,
                    line_number,
                    "raw pointer use requires a memory-safety review",
                ));
            }
            if contains_undefined_value(line) {
                findings.extend(finding(
                    "odin-undefined-value",
                    path,
                    line_number,
                    "undefined value marker bypasses initialization",
                ));
            }
        }
    }
    findings
}

fn contains_identifier_call(line: &str, identifier: &str) -> bool {
    identifier_positions(line, identifier).any(|index| {
        line[index + identifier.len()..]
            .trim_start()
            .starts_with('(')
    })
}

fn contains_identifier(line: &str, identifier: &str) -> bool {
    identifier_positions(line, identifier).next().is_some()
}

fn identifier_positions<'a>(
    line: &'a str,
    identifier: &'a str,
) -> impl Iterator<Item = usize> + 'a {
    let bytes = line.as_bytes();
    line.match_indices(identifier)
        .map(|(index, _)| index)
        .filter(move |index| {
            let before = bytes[..*index].last().copied();
            let after = bytes[*index + identifier.len()..].first().copied();
            before.is_none_or(is_identifier_boundary) && after.is_none_or(is_identifier_boundary)
        })
}

fn is_identifier_boundary(byte: u8) -> bool {
    !byte.is_ascii_alphanumeric() && byte != b'_'
}

fn contains_undefined_value(line: &str) -> bool {
    let bytes = line.as_bytes();
    line.match_indices("---").any(|(index, _)| {
        let before = line[..index].trim_end().as_bytes().last().copied();
        let after = bytes[index + 3..].first().copied();
        matches!(before, Some(b'=' | b':'))
            && after.is_none_or(|byte| byte.is_ascii_whitespace() || matches!(byte, b',' | b'}'))
    })
}

fn finding(code: &str, path: &str, line: usize, message: &str) -> Option<Finding> {
    let metadata = rule_catalog::metadata(code)?;
    Some(Finding {
        code: code.to_owned(),
        severity: Severity::parse(metadata.default_severity)?,
        path: path.to_owned(),
        line,
        end_line: line,
        message: message.to_owned(),
        confidence: "high".to_owned(),
        why: metadata.why.to_owned(),
        suggestion: metadata.suggestion.to_owned(),
        related_files: Vec::new(),
        snippet: String::new(),
        code_flow: Vec::new(),
    })
}

fn odin_code(source: &str) -> String {
    OdinMasker::new(source).masked()
}

#[derive(Clone, Copy)]
enum OdinScanState {
    Code,
    LineComment,
    BlockComment(usize),
    Quote(u8),
}

struct OdinMasker<'a> {
    source: &'a [u8],
    code: Vec<u8>,
    index: usize,
    state: OdinScanState,
}

impl<'a> OdinMasker<'a> {
    fn new(source: &'a str) -> Self {
        Self {
            source: source.as_bytes(),
            code: source.as_bytes().to_vec(),
            index: 0,
            state: OdinScanState::Code,
        }
    }

    fn masked(mut self) -> String {
        while self.index < self.source.len() {
            self.scan_next();
        }
        String::from_utf8(self.code).unwrap_or_default()
    }

    fn scan_next(&mut self) {
        if self.source[self.index] == b'\n' {
            if !matches!(self.state, OdinScanState::BlockComment(_)) {
                self.state = OdinScanState::Code;
            }
            self.index += 1;
            return;
        }
        match self.state {
            OdinScanState::Code => self.scan_code(),
            OdinScanState::LineComment => self.mask_byte(),
            OdinScanState::BlockComment(depth) => self.scan_block_comment(depth),
            OdinScanState::Quote(delimiter) => self.scan_quote(delimiter),
        }
    }

    fn scan_code(&mut self) {
        let byte = self.source[self.index];
        if self.starts_with(b'/', b'/') {
            self.mask_pair();
            self.state = OdinScanState::LineComment;
        } else if self.starts_with(b'/', b'*') {
            self.mask_pair();
            self.state = OdinScanState::BlockComment(1);
        } else if matches!(byte, b'"' | b'\'' | b'`') {
            self.code[self.index] = b' ';
            self.index += 1;
            self.state = OdinScanState::Quote(byte);
        } else {
            self.index += 1;
        }
    }

    fn scan_block_comment(&mut self, depth: usize) {
        self.code[self.index] = b' ';
        if self.starts_with(b'/', b'*') {
            self.mask_pair();
            self.state = OdinScanState::BlockComment(depth + 1);
        } else if self.starts_with(b'*', b'/') {
            self.mask_pair();
            self.state = if depth == 1 {
                OdinScanState::Code
            } else {
                OdinScanState::BlockComment(depth - 1)
            };
        } else {
            self.index += 1;
        }
    }

    fn scan_quote(&mut self, delimiter: u8) {
        let byte = self.source[self.index];
        self.code[self.index] = b' ';
        if delimiter != b'`'
            && byte == b'\\'
            && self
                .source
                .get(self.index + 1)
                .is_some_and(|next| *next != b'\n')
        {
            self.code[self.index + 1] = b' ';
            self.index += 2;
        } else {
            self.index += 1;
            if byte == delimiter {
                self.state = OdinScanState::Code;
            }
        }
    }

    fn starts_with(&self, first: u8, second: u8) -> bool {
        self.source[self.index] == first && self.source.get(self.index + 1).copied() == Some(second)
    }

    fn mask_byte(&mut self) {
        self.code[self.index] = b' ';
        self.index += 1;
    }

    fn mask_pair(&mut self) {
        self.code[self.index] = b' ';
        self.code[self.index + 1] = b' ';
        self.index += 2;
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use super::analyze;
    use crate::source_analysis_context::SourceAnalysisContext;

    #[test]
    fn reports_failure_and_memory_safety_boundaries() {
        let sources = BTreeMap::from([(
            "main.odin".to_owned(),
            "package main\n\nrun :: proc() {\n    panic(\"failed\")\n    bits := transmute(u32)f32_value\n    pointer: rawptr = raw_data(value)\n    bytes: [16]u8 = ---\n}\n"
                .to_owned(),
        )]);
        let context = SourceAnalysisContext::unchanged(&sources);

        let findings = analyze(&context);

        assert_eq!(
            findings
                .iter()
                .map(|finding| finding.code.as_str())
                .collect::<Vec<_>>(),
            vec![
                "odin-panic-call",
                "odin-transmute",
                "odin-raw-pointer",
                "odin-undefined-value",
            ]
        );
        assert_eq!(
            findings
                .iter()
                .map(|finding| finding.line)
                .collect::<Vec<_>>(),
            vec![4, 5, 6, 7]
        );
        assert!(findings.iter().all(|finding| finding.confidence == "high"));
    }

    #[test]
    fn ignores_rule_text_in_nested_comments_and_literals() {
        let sources = BTreeMap::from([(
            "main.odin".to_owned(),
            "package main\n\nrun :: proc() {\n    // panic(\"example\")\n    text := \"transmute(rawptr)\"\n    raw := `value = ---`\n    /* rawptr\n       /* panic(\"nested\") */\n       transmute(u32)value\n    */\n}\n"
                .to_owned(),
        )]);
        let context = SourceAnalysisContext::unchanged(&sources);

        assert!(analyze(&context).is_empty());
    }
}