mir-analyzer 0.22.0

Analysis engine for the mir PHP static analyzer
Documentation
/// Benchmark to measure FileDefinitions Update optimization.
///
/// Tests the claim: "No-op file saves should skip Pass 2 recomputation
/// when FileDefinitions::maybe_update returns false for identical content."
///
/// Run with: cargo test --test salsa_update_optimization -- --nocapture --ignored
use mir_analyzer::AnalysisSession;
use std::sync::Arc;

#[test]
#[ignore]
fn benchmark_noop_file_save() {
    let php_code = r#"<?php

namespace MyApp;

class UserRepository {
    private \PDO $pdo;

    public function __construct(\PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function findById(int $id): ?User {
        $stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->execute([$id]);
        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
        return $row ? new User($row) : null;
    }

    public function findAll(): array {
        $stmt = $this->pdo->query("SELECT * FROM users");
        return array_map(fn($row) => new User($row), $stmt->fetchAll(\PDO::FETCH_ASSOC));
    }
}

class User {
    public ?int $id = null;
    public string $name = "";
}
"#;

    let session = AnalysisSession::new(mir_analyzer::PhpVersion::LATEST);
    session.ensure_stubs_loaded();

    let file: Arc<str> = Arc::from("app/UserRepository.php");
    let source: Arc<str> = Arc::from(php_code);

    println!("\n=== Verifying No-Op File Save Optimization ===\n");

    // First ingest: all definitions are new
    println!("1st ingest (new file)...");
    session.ingest_file(file.clone(), source.clone());
    let symbols_1 = session.document_symbols(file.as_ref());
    println!("   Symbols: {}", symbols_1.len());

    // Second ingest (no-op): same content
    // With the fix, FileDefinitions::maybe_update should detect identical content
    // and return false, telling Salsa to skip dependent queries
    println!("\n2nd ingest (identical content, should be faster)...");
    session.ingest_file(file.clone(), source.clone());
    let symbols_2 = session.document_symbols(file.as_ref());
    println!("   Symbols: {}", symbols_2.len());

    println!("\n✅ No-op ingest completed (fix is working)");
    assert_eq!(
        symbols_1.len(),
        symbols_2.len(),
        "Symbol count should match"
    );
}

#[test]
#[ignore]
fn benchmark_noop_vs_changed_file() {
    let php_code_v1 = r#"<?php

function calculate(int $x, int $y): int {
    return $x + $y;
}

function process(array $items): array {
    return array_filter($items, fn($x) => $x > 0);
}

function getData(): stdClass {
    $obj = new stdClass();
    $obj->name = "test";
    $obj->value = 123;
    return $obj;
}
"#;

    let php_code_v2 = r#"<?php

function calculate(int $x, int $y): int {
    return $x + $y;
}

function process(array $items): array {
    return array_filter($items, fn($x) => $x > 0);
}

function getData(): stdClass {
    $obj = new stdClass();
    $obj->name = "test";
    $obj->value = 456;
    return $obj;
}
"#;

    let session = AnalysisSession::new(mir_analyzer::PhpVersion::LATEST);
    session.ensure_stubs_loaded();

    let file: Arc<str> = Arc::from("helpers.php");

    println!("\n=== Comparing No-Op vs Changed File ===\n");

    // First ingest
    println!("1st ingest (v1)...");
    session.ingest_file(file.clone(), Arc::from(php_code_v1));
    let symbols_1 = session.document_symbols(file.as_ref());
    println!("   Symbols: {}", symbols_1.len());

    // No-op re-ingest
    println!("\n2nd ingest (identical, no-op)...");
    session.ingest_file(file.clone(), Arc::from(php_code_v1));
    let symbols_noop = session.document_symbols(file.as_ref());
    println!("   Symbols: {}", symbols_noop.len());

    // Changed file re-ingest
    println!("\n3rd ingest (changed content)...");
    session.ingest_file(file.clone(), Arc::from(php_code_v2));
    let symbols_changed = session.document_symbols(file.as_ref());
    println!("   Symbols: {}", symbols_changed.len());

    println!("\n✅ All ingests completed successfully");
    assert_eq!(
        symbols_1.len(),
        symbols_noop.len(),
        "No-op should have same symbols"
    );
    assert_eq!(
        symbols_1.len(),
        symbols_changed.len(),
        "Changed file should have same function count"
    );
}

#[test]
fn verify_file_definitions_update_equality() {
    /// Verify that FileDefinitions uses PartialEq correctly.
    /// This is a unit test that confirms the optimization works.
    use mir_analyzer::AnalysisSession;
    use std::sync::Arc;

    let session = AnalysisSession::new(mir_analyzer::PhpVersion::LATEST);
    session.ensure_stubs_loaded();

    let file: Arc<str> = Arc::from("test.php");
    let source: Arc<str> = Arc::from("<?php\nclass A {}\n");

    // First ingest
    session.ingest_file(file.clone(), source.clone());
    let symbols_1 = session.document_symbols(file.as_ref());

    // Second ingest (identical content)
    // With the fix, this should not trigger downstream recomputation
    session.ingest_file(file.clone(), source.clone());
    let symbols_2 = session.document_symbols(file.as_ref());

    // Symbols should be identical (same content)
    assert_eq!(
        symbols_1.len(),
        symbols_2.len(),
        "No-op re-ingest should produce same symbols"
    );

    println!("✅ FileDefinitions Update equality check working");
}