mago-analyzer 1.17.0

A PHP static analyzer that can detect type errors in PHP code, and provide suggestions for fixing them.
Documentation
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::sync::LazyLock;

use foldhash::HashSet;

use bumpalo::Bump;
use mago_analyzer::Analyzer;
use mago_analyzer::analysis_result::AnalysisResult;
use mago_analyzer::plugin::PluginRegistry;
use mago_analyzer::settings::Settings;
use mago_atom::AtomSet;
use mago_codex::metadata::CodebaseMetadata;
use mago_codex::populator::populate_codebase;
use mago_codex::scanner::scan_program;
use mago_database::DatabaseReader;
use mago_database::file::File;
use mago_names::resolver::NameResolver;
use mago_prelude::Prelude;
use mago_syntax::parser::parse_file;

static PRELUDE: LazyLock<Prelude> = LazyLock::new(Prelude::build);
static PLUGIN_REGISTRY: LazyLock<PluginRegistry> = LazyLock::new(PluginRegistry::with_library_providers);

#[derive(Debug, Clone)]
pub struct TestCase<'a> {
    name: &'a str,
    content: &'a str,
    settings: Option<Settings>,
}

impl<'a> TestCase<'a> {
    #[must_use]
    pub fn new(name: &'a str, content: &'a str) -> Self {
        Self { name, content, settings: None }
    }

    #[must_use]
    pub fn settings(mut self, settings: Settings) -> Self {
        self.settings = Some(settings);
        self
    }

    pub fn run(self) {
        run_test_case_inner(self);
    }
}

#[must_use]
pub fn default_test_settings() -> Settings {
    Settings {
        find_unused_expressions: true,
        find_unused_definitions: true,
        check_throws: true,
        allow_possibly_undefined_array_keys: false,
        strict_list_index_checks: true,
        check_property_initialization: true,
        ..Default::default()
    }
}

fn run_test_case_inner(config: TestCase) {
    let Prelude { mut database, mut metadata, mut symbol_references } = PRELUDE.clone();

    let file = File::ephemeral(Cow::Owned(config.name.to_string()), Cow::Owned(config.content.to_string()));
    let file_id = database.add(file);
    let source_file = database.get_ref(&file_id).expect("File just added should exist");

    let arena = Bump::new();
    let program = parse_file(&arena, source_file);
    assert!(!program.has_errors(), "Parse failed: {:?}", program.errors);

    let resolver = NameResolver::new(&arena);
    let resolved_names = resolver.resolve(program);

    metadata.extend(scan_program(&arena, source_file, program, &resolved_names));

    populate_codebase(&mut metadata, &mut symbol_references, AtomSet::default(), HashSet::default());

    let settings = config.settings.unwrap_or_else(default_test_settings);

    let mut analysis_result = AnalysisResult::new(symbol_references);
    let analyzer = Analyzer::new(&arena, source_file, &resolved_names, &metadata, &PLUGIN_REGISTRY, settings);

    let analysis_run_result = analyzer.analyze(program, &mut analysis_result);

    if let Err(err) = analysis_run_result {
        panic!("Test '{}': Expected analysis to succeed, but it failed with an error: {}", config.name, err);
    }

    verify_reported_issues(config.name, analysis_result, metadata);
}

fn verify_reported_issues(test_name: &str, mut analysis_result: AnalysisResult, mut codebase: CodebaseMetadata) {
    let mut actual_issues_collected = std::mem::take(&mut analysis_result.issues);

    actual_issues_collected.extend(codebase.take_issues(true));

    let mut actual_issue_counts: BTreeMap<String, usize> = BTreeMap::new();
    for actual_issue in &actual_issues_collected {
        let Some(issue_code) = actual_issue.code.clone() else {
            panic!("Analyzer returned an issue with no code: {actual_issue:?}");
        };

        *actual_issue_counts.entry(issue_code).or_insert(0) += 1;
    }

    if !actual_issue_counts.is_empty() {
        let mut discrepancies = Vec::new();
        for (actual_kind, &actual_count) in &actual_issue_counts {
            discrepancies.push(format!("- Unexpected issue(s) `{}`: found {}.", actual_kind.as_str(), actual_count));
        }

        let mut panic_message = format!("Test '{test_name}' failed with issue discrepancies:\n");
        for d in discrepancies {
            panic_message.push_str(&format!("  {d}\n"));
        }

        panic!("{}", panic_message);
    }
}