mir-analyzer 0.60.0

Analysis engine for the mir PHP static analyzer
Documentation
// crates/mir-analyzer/tests/type_env.rs
use mir_analyzer::ScopeId;
use std::sync::Arc;

#[test]
fn scope_id_function_equality() {
    let a = ScopeId::Function {
        file: Arc::from("foo.php"),
        name: Arc::from("myFn"),
    };
    let b = ScopeId::Function {
        file: Arc::from("foo.php"),
        name: Arc::from("myFn"),
    };
    assert_eq!(a, b);
}

#[test]
fn analyze_result_has_type_envs_field() {
    let result = mir_analyzer::analyze_source("<?php\n");
    // just verifying the field exists and is empty for a trivial source
    assert!(result.type_envs.is_empty());
}

#[test]
fn returns_type_env_for_function_scope() {
    let src = "<?php\nfunction myFn(): void {\n    $greeting = 'hello';\n}\n";
    let result = mir_analyzer::analyze_source(src);
    let scope = result.type_envs.iter().find(|(k, _)| {
        matches!(k, mir_analyzer::ScopeId::Function { name, .. } if name.as_ref() == "myFn")
    });
    assert!(scope.is_some(), "Expected a TypeEnv for function myFn");
    let env = scope.unwrap().1;
    assert!(
        env.get_var("greeting").is_some(),
        "Expected $greeting in TypeEnv"
    );
}

#[test]
fn returns_type_env_for_method_scope() {
    let src = "<?php\nclass MyClass {\n    public function handle(): void {\n        $result = 42;\n    }\n}\n";
    let result = mir_analyzer::analyze_source(src);
    let scope = result.type_envs.iter().find(|(k, _)| {
        matches!(k, mir_analyzer::ScopeId::Method { method, .. } if method.as_ref() == "handle")
    });
    assert!(scope.is_some(), "Expected a TypeEnv for method handle");
    let env = scope.unwrap().1;
    assert!(
        env.get_var("result").is_some(),
        "Expected $result in TypeEnv"
    );
}

#[test]
fn get_var_returns_none_for_unknown_variable() {
    let src = "<?php\nfunction f(): void {\n    $x = 1;\n}\n";
    let result = mir_analyzer::analyze_source(src);
    let scope = result.type_envs.iter().find(
        |(k, _)| matches!(k, mir_analyzer::ScopeId::Function { name, .. } if name.as_ref() == "f"),
    );
    assert!(scope.is_some(), "Expected a TypeEnv for function f");
    let env = scope.unwrap().1;
    assert!(env.get_var("nonexistent").is_none());
}

#[test]
fn var_names_lists_all_variables_in_scope() {
    let src = "<?php\nfunction f(): void {\n    $a = 1;\n    $b = 'hello';\n}\n";
    let result = mir_analyzer::analyze_source(src);
    let env = result.type_envs.values().next().unwrap();
    let names: Vec<&str> = env.var_names().collect();
    assert!(names.contains(&"a"), "Expected 'a' in var_names");
    assert!(names.contains(&"b"), "Expected 'b' in var_names");
}

#[test]
fn issue_kind_accessible_via_mir_analyzer() {
    // Verify IssueKind can be used directly from mir_analyzer without importing mir_issues
    let _kind = mir_analyzer::IssueKind::UndefinedVariable { name: "foo".into() };
    let _sev = mir_analyzer::Severity::Error;
}

// ---------------------------------------------------------------------------
// analyze_source's typed path (analyze_bodies_typed) had drifted out of sync
// with the untyped batch/LSP path (analyze_bodies): several checks ran on one
// but not the other. Each test below exercises analyze_source directly, the
// only entry point that reaches the typed path.
// ---------------------------------------------------------------------------

#[test]
fn analyze_source_flags_attribute_on_function() {
    let src = "<?php\n#[Attribute]\nfunction foo(): void {}\n";
    let result = mir_analyzer::analyze_source(src);
    assert!(
        result
            .issues
            .iter()
            .any(|i| i.kind.name() == "InvalidAttribute"),
        "expected InvalidAttribute for #[Attribute] on a function, got: {:?}",
        result
            .issues
            .iter()
            .map(|i| i.kind.name())
            .collect::<Vec<_>>()
    );
}

#[test]
fn analyze_source_flags_attribute_on_abstract_class() {
    let src = "<?php\n#[Attribute]\nabstract class Foo {}\n";
    let result = mir_analyzer::analyze_source(src);
    assert!(
        result
            .issues
            .iter()
            .any(|i| i.kind.name() == "InvalidAttribute"),
        "expected InvalidAttribute for #[Attribute] on an abstract class, got: {:?}",
        result
            .issues
            .iter()
            .map(|i| i.kind.name())
            .collect::<Vec<_>>()
    );
}

#[test]
fn analyze_source_flags_attribute_on_trait() {
    let src = "<?php\n#[Attribute]\ntrait Foo {}\n";
    let result = mir_analyzer::analyze_source(src);
    assert!(
        result
            .issues
            .iter()
            .any(|i| i.kind.name() == "InvalidAttribute"),
        "expected InvalidAttribute for #[Attribute] on a trait, got: {:?}",
        result
            .issues
            .iter()
            .map(|i| i.kind.name())
            .collect::<Vec<_>>()
    );
}

#[test]
fn analyze_source_flags_undefined_class_in_param_default() {
    let src = "<?php\nfunction foo($x = NoSuchClass::VALUE): void {}\n";
    let result = mir_analyzer::analyze_source(src);
    assert!(
        result
            .issues
            .iter()
            .any(|i| i.kind.name() == "UndefinedClass"),
        "expected UndefinedClass for an undefined class in a param default, got: {:?}",
        result
            .issues
            .iter()
            .map(|i| i.kind.name())
            .collect::<Vec<_>>()
    );
}

#[test]
fn analyze_source_flags_duplicate_function_declaration() {
    let src = "<?php\nfunction foo(): void {}\nfunction foo(): void {}\n";
    let result = mir_analyzer::analyze_source(src);
    assert!(
        result
            .issues
            .iter()
            .any(|i| i.kind.name() == "DuplicateFunction"),
        "expected DuplicateFunction for a re-declared function, got: {:?}",
        result
            .issues
            .iter()
            .map(|i| i.kind.name())
            .collect::<Vec<_>>()
    );
}