car-ast 0.14.0

Tree-sitter AST parsing for code-aware inference
Documentation
//! Integration test: parse the actual car-rs codebase with tree-sitter.
//! This proves car-ast works on real-world Rust code, not just toy snippets.

use car_ast::{parse, parse_file, Language, ProjectIndex};
use std::path::Path;

/// Parse a real Rust file from this workspace.
#[test]
fn parse_real_rust_file() {
    // Read our own lib.rs
    let source =
        std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("src/lib.rs")).unwrap();

    let parsed = parse(&source, Language::Rust).unwrap();

    // Should find the `parse` and `parse_file` public functions
    let parse_fns = parsed.find_symbol("parse");
    assert!(
        !parse_fns.is_empty(),
        "should find the `parse` function in car-ast/src/lib.rs"
    );

    let parse_file_fns = parsed.find_symbol("parse_file");
    assert!(
        !parse_file_fns.is_empty(),
        "should find `parse_file` function"
    );

    // Should find extract_source, extract_context, diff_symbols, etc.
    let all = parsed.all_symbols();
    assert!(
        all.len() >= 4,
        "should have at least 4 symbols, got {}",
        all.len()
    );

    // Imports should be present
    assert!(!parsed.imports.is_empty(), "should have imports");
}

/// Parse the types.rs file and verify struct extraction.
#[test]
fn parse_types_file() {
    let source =
        std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("src/types.rs"))
            .unwrap();

    let parsed = parse(&source, Language::Rust).unwrap();

    // Should find Language, Symbol, SymbolKind, Span, Import, ParsedFile, etc.
    let language = parsed.find_symbol("Language");
    assert!(!language.is_empty(), "should find Language enum");

    let symbol = parsed.find_symbol("Symbol");
    assert!(!symbol.is_empty(), "should find Symbol struct");

    let span = parsed.find_symbol("Span");
    assert!(!span.is_empty(), "should find Span struct");

    let parsed_file = parsed.find_symbol("ParsedFile");
    assert!(!parsed_file.is_empty(), "should find ParsedFile struct");

    // ParsedFile should have impl with methods (find_symbol, find_symbol_fuzzy, all_symbols)
    let impls: Vec<_> = parsed
        .symbols
        .iter()
        .filter(|s| s.kind == car_ast::SymbolKind::Impl && s.name == "ParsedFile")
        .collect();
    assert!(!impls.is_empty(), "should find ParsedFile impl block");
    let methods: Vec<_> = impls[0].children.iter().map(|m| m.name.as_str()).collect();
    assert!(
        methods.contains(&"find_symbol"),
        "ParsedFile should have find_symbol method, got {:?}",
        methods
    );
    assert!(
        methods.contains(&"all_symbols"),
        "ParsedFile should have all_symbols method"
    );
}

/// Index the car-ast crate itself and verify cross-file references.
#[test]
fn index_car_ast_crate() {
    let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
    let index = ProjectIndex::build(&root);
    let stats = index.stats();

    // Should have parsed multiple files (lib.rs, types.rs, index.rs, languages/*.rs)
    assert!(
        stats.files >= 5,
        "should parse >= 5 files, got {}",
        stats.files
    );

    // Should have many symbols
    assert!(
        stats.symbols >= 20,
        "should have >= 20 symbols, got {}",
        stats.symbols
    );

    // Should have cross-file references (e.g., languages/rust.rs uses types from types.rs)
    assert!(
        stats.references > 0,
        "should have cross-file references, got {}",
        stats.references
    );

    // Find Symbol struct across the index
    let symbols = index.find("Symbol");
    assert!(!symbols.is_empty(), "should find Symbol in index");

    // Find who references Span (should be used in many files)
    let span_callers = index.callers_of("Span");
    assert!(
        !span_callers.is_empty(),
        "Span should be referenced from other files"
    );

    println!("Index stats: {}", stats);
    println!("Span referenced from {} locations", span_callers.len());
    for r in &span_callers {
        println!("  {} ({}) -> Span", r.from_file, r.from_symbol);
    }
}

/// Parse the Rust language extractor to verify it handles its own code.
#[test]
fn parse_rust_extractor() {
    let source = std::fs::read_to_string(
        Path::new(env!("CARGO_MANIFEST_DIR")).join("src/languages/rust.rs"),
    )
    .unwrap();

    let parsed = parse_file(&source, "rust.rs").unwrap();

    // Should find the extract function
    let extract = parsed.find_symbol("extract");
    assert!(!extract.is_empty(), "should find `extract` function");

    // Should find helper functions
    let helpers = parsed.find_symbol_fuzzy("extract_");
    assert!(
        helpers.len() >= 3,
        "should find multiple extract_* helpers, got {}",
        helpers.len()
    );

    // Verify signatures are captured (not empty)
    for sym in parsed.all_symbols() {
        assert!(
            !sym.signature.is_empty(),
            "symbol {} should have a non-empty signature",
            sym.name
        );
    }
}

/// Parse Python code to verify multi-language support works.
#[test]
fn parse_real_python() {
    // A realistic Python file (not from our codebase, but representative)
    let source = r#"
import asyncio
from dataclasses import dataclass, field
from typing import Optional, List

@dataclass
class AgentConfig:
    """Configuration for an AI agent."""
    name: str
    model: str = "gpt-4"
    temperature: float = 0.7
    max_tokens: int = 4096
    tools: List[str] = field(default_factory=list)

class Agent:
    """An AI agent that can execute tasks."""

    def __init__(self, config: AgentConfig):
        self.config = config
        self._history: List[dict] = []

    async def run(self, prompt: str) -> str:
        """Run the agent with a prompt."""
        self._history.append({"role": "user", "content": prompt})
        return await self._generate()

    async def _generate(self) -> str:
        """Generate a response using the configured model."""
        return "response"

    @property
    def history(self) -> List[dict]:
        return self._history

async def create_agent(name: str, model: Optional[str] = None) -> Agent:
    """Factory function for creating agents."""
    config = AgentConfig(name=name, model=model or "gpt-4")
    return Agent(config)

MAX_RETRIES = 3
"#;

    let parsed = parse(source, Language::Python).unwrap();

    // Classes
    let agent_config = parsed.find_symbol("AgentConfig");
    assert!(!agent_config.is_empty(), "should find AgentConfig class");
    assert_eq!(agent_config[0].kind, car_ast::SymbolKind::Class);

    let agent = parsed.find_symbol("Agent");
    assert!(!agent.is_empty(), "should find Agent class");

    // Agent should have methods: __init__, run, _generate, history
    let agent_class = &parsed
        .symbols
        .iter()
        .find(|s| s.name == "Agent" && s.kind == car_ast::SymbolKind::Class)
        .unwrap();
    assert!(
        agent_class.children.len() >= 3,
        "Agent should have >= 3 methods, got {}",
        agent_class.children.len()
    );

    // Standalone function
    let create = parsed.find_symbol("create_agent");
    assert!(!create.is_empty(), "should find create_agent function");
    assert_eq!(create[0].kind, car_ast::SymbolKind::Function);

    // Constant
    let max_retries = parsed.find_symbol("MAX_RETRIES");
    assert!(!max_retries.is_empty(), "should find MAX_RETRIES constant");
    assert_eq!(max_retries[0].kind, car_ast::SymbolKind::Const);

    // Imports
    assert!(parsed.imports.len() >= 2, "should have imports");
}

/// Parse TypeScript to verify TS grammar works.
#[test]
fn parse_real_typescript() {
    let source = r#"
import { EventEmitter } from 'events';

interface Plugin {
    name: string;
    init(): Promise<void>;
    destroy?(): void;
}

type PluginFactory = (config: Record<string, unknown>) => Plugin;

export class Runtime extends EventEmitter {
    private plugins: Map<string, Plugin> = new Map();

    constructor(private config: RuntimeConfig) {
        super();
    }

    async loadPlugin(factory: PluginFactory): Promise<void> {
        const plugin = factory(this.config);
        this.plugins.set(plugin.name, plugin);
        await plugin.init();
        this.emit('plugin:loaded', plugin.name);
    }

    getPlugin(name: string): Plugin | undefined {
        return this.plugins.get(name);
    }
}

export async function createRuntime(config: RuntimeConfig): Promise<Runtime> {
    return new Runtime(config);
}
"#;

    let parsed = parse(source, Language::TypeScript).unwrap();

    // Interface
    let plugin = parsed.find_symbol("Plugin");
    assert!(!plugin.is_empty(), "should find Plugin interface");

    // Type alias
    let factory = parsed.find_symbol("PluginFactory");
    assert!(!factory.is_empty(), "should find PluginFactory type alias");

    // Class with methods
    let runtime = parsed.find_symbol("Runtime");
    assert!(!runtime.is_empty(), "should find Runtime class");

    let runtime_class = &parsed
        .symbols
        .iter()
        .find(|s| s.name == "Runtime" && s.kind == car_ast::SymbolKind::Class)
        .unwrap();
    let method_names: Vec<_> = runtime_class
        .children
        .iter()
        .map(|m| m.name.as_str())
        .collect();
    assert!(
        method_names.contains(&"loadPlugin"),
        "should find loadPlugin method, got {:?}",
        method_names
    );
    assert!(
        method_names.contains(&"getPlugin"),
        "should find getPlugin method"
    );

    // Exported function
    let create = parsed.find_symbol("createRuntime");
    assert!(!create.is_empty(), "should find createRuntime function");
}

/// Parse Go to verify Go grammar works.
#[test]
fn parse_real_go() {
    let source = r#"
package server

import (
    "context"
    "net/http"
    "sync"
)

// Server handles HTTP requests.
type Server struct {
    mu      sync.Mutex
    handler http.Handler
    addr    string
}

// Handler defines the request handling interface.
type Handler interface {
    ServeHTTP(w http.ResponseWriter, r *http.Request)
    Health() error
}

// NewServer creates a new server instance.
func NewServer(addr string, handler http.Handler) *Server {
    return &Server{addr: addr, handler: handler}
}

// Start begins listening for connections.
func (s *Server) Start(ctx context.Context) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    return http.ListenAndServe(s.addr, s.handler)
}

const MaxConnections = 1000
"#;

    let parsed = parse(source, Language::Go).unwrap();

    // Struct
    let server = parsed.find_symbol("Server");
    assert!(!server.is_empty(), "should find Server struct");

    // Interface
    let handler = parsed.find_symbol("Handler");
    assert!(!handler.is_empty(), "should find Handler interface");

    // Functions
    let new_server = parsed.find_symbol("NewServer");
    assert!(!new_server.is_empty(), "should find NewServer function");

    // Method with receiver
    let start = parsed.find_symbol("Start");
    assert!(!start.is_empty(), "should find Start method");
    assert_eq!(start[0].kind, car_ast::SymbolKind::Method);
    assert!(start[0].parent.is_some(), "Start should have a receiver");

    // Imports
    assert!(!parsed.imports.is_empty(), "should have imports");
}