codesearch 0.1.15

A fast, intelligent CLI tool with multiple search modes (regex, fuzzy, semantic), code analysis, and dead code detection for popular programming languages
Documentation
//! Context Detection for Structural Search
//!
//! Detects whether a line of code is primarily:
//! - Code (executable logic)
//! - Comments (documentation, inline notes)
//! - Strings (literal text values)
//!
//! This enables `--only-code`, `--only-comments`, and `--only-strings` filters.

use crate::types::ContextFilter;

/// Classify the context of a single line.
///
/// Returns the dominant context type. A line can only have one dominant context.
/// Priority: Comments > Code > Strings.
/// (Comments take precedence because even `println!("//");` is still code, but
/// a line that is `// comment` is unambiguously a comment.
/// Code takes precedence over Strings because a line like `println!("hello")`
/// is executable code, not merely a string literal.)
pub fn classify_line(line: &str) -> LineContext {
    let trimmed = line.trim_start();

    // 1. Comment detection (highest priority)
    if is_comment_line(trimmed) {
        return LineContext::Comment;
    }

    // 2. Check if line is inside a multi-line comment block
    if trimmed.starts_with("*") || trimmed.starts_with("*/") {
        return LineContext::Comment;
    }

    // 3. Code detection — if the line contains typical code tokens, it's code
    if has_code_tokens(line) {
        return LineContext::Code;
    }

    // 4. String literal detection (only if no code tokens are present)
    if has_string_literal(line) {
        return LineContext::String;
    }

    LineContext::Code
}

/// Determine whether a search result should be included based on context filter.
pub fn should_include(context: LineContext, filter: Option<&ContextFilter>) -> bool {
    match filter {
        None => true,
        Some(ContextFilter::Code) => context == LineContext::Code,
        Some(ContextFilter::Comments) => context == LineContext::Comment,
        Some(ContextFilter::Strings) => context == LineContext::String,
    }
}

/// Possible contexts for a line of source code
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineContext {
    Code,
    Comment,
    String,
}

/// Check if a trimmed line is a comment.
fn is_comment_line(trimmed: &str) -> bool {
    trimmed.starts_with("//")
        || trimmed.starts_with('#')
        || trimmed.starts_with("/*")
        || trimmed.starts_with("*")
}

/// Check if a line contains typical code tokens (function calls, keywords, etc.)
fn has_code_tokens(line: &str) -> bool {
    let code_indicators = [
        "fn ",
        "def ",
        "function ",
        "class ",
        "struct ",
        "impl ",
        "let ",
        "const ",
        "var ",
        "mut ",
        "return",
        "if ",
        "else ",
        "for ",
        "while ",
        "loop ",
        "match ",
        "use ",
        "mod ",
        "pub ",
        "priv ",
        "import ",
        "from ",
        "using ",
        "println!",
        "print!",
        "assert!",
        "assert_eq!",
        "(",
        ")",
        "{",
        "}",
        "[",
        "]",
        ";",
        "=>",
        "->",
        "::",
    ];
    let lower = line.to_lowercase();
    code_indicators.iter().any(|&token| lower.contains(token))
}

/// Check if a line contains a string literal.
///
/// Uses a simple heuristic: does the line contain quoted text that is not
/// inside a single-line comment?
fn has_string_literal(line: &str) -> bool {
    if let Some(comment_pos) = line.find("//") {
        let before_comment = &line[..comment_pos];
        has_unescaped_quote(before_comment)
    } else {
        has_unescaped_quote(line)
    }
}

/// Check if a string has an unescaped quote character.
fn has_unescaped_quote(s: &str) -> bool {
    let bytes = s.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == b'"' || bytes[i] == b'\'' {
            return true;
        }
        i += 1;
    }
    false
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_classify_comment() {
        assert_eq!(classify_line("// This is a comment"), LineContext::Comment);
        assert_eq!(classify_line("  // Indented comment"), LineContext::Comment);
        assert_eq!(classify_line("# Python comment"), LineContext::Comment);
        assert_eq!(
            classify_line("/* block comment start"),
            LineContext::Comment
        );
        assert_eq!(classify_line(" * continuation"), LineContext::Comment);
    }

    #[test]
    fn test_classify_string() {
        // Pure string content without code tokens
        assert_eq!(classify_line(r#""hello world""#), LineContext::String);
        assert_eq!(classify_line("'a'"), LineContext::String);
    }

    #[test]
    fn test_classify_code_with_strings() {
        // Code containing string literals is still classified as Code
        assert_eq!(classify_line(r#"println!("hello");"#), LineContext::Code);
        assert_eq!(classify_line("let x = 'a';"), LineContext::Code);
    }

    #[test]
    fn test_classify_code() {
        assert_eq!(classify_line("fn main() {"), LineContext::Code);
        assert_eq!(classify_line("let x = 42;"), LineContext::Code);
        assert_eq!(classify_line("}"), LineContext::Code);
    }

    #[test]
    fn test_should_include_filter() {
        assert!(should_include(LineContext::Code, None));
        assert!(should_include(
            LineContext::Code,
            Some(&ContextFilter::Code)
        ));
        assert!(!should_include(
            LineContext::Comment,
            Some(&ContextFilter::Code)
        ));
        assert!(should_include(
            LineContext::Comment,
            Some(&ContextFilter::Comments)
        ));
        assert!(!should_include(
            LineContext::Code,
            Some(&ContextFilter::Comments)
        ));
        assert!(should_include(
            LineContext::String,
            Some(&ContextFilter::Strings)
        ));
    }

    #[test]
    fn test_string_in_comment_not_counted() {
        // A line that is purely a comment should be classified as comment,
        // even if it contains quote-like text
        assert_eq!(
            classify_line(r#"// println!("hello")"#),
            LineContext::Comment
        );
    }
}