#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::sync::LazyLock;
use foldhash::HashSet;
use mago_allocator::LocalArena;
use mago_analyzer::Analyzer;
use mago_analyzer::analysis_result::AnalysisResult;
use mago_analyzer::plugin::PluginRegistry;
use mago_analyzer::settings::Settings;
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;
use mago_word::WordSet;
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<'src> {
name: &'src str,
content: &'src [u8],
settings: Option<Settings>,
}
impl<'src> TestCase<'src> {
#[must_use]
pub fn new(name: &'src str, content: &'src [u8]) -> 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()
}
}
#[must_use]
pub fn infection_like_settings() -> Settings {
Settings {
find_unused_expressions: true,
find_unused_definitions: true,
find_unused_parameters: true,
check_throws: true,
analyze_dead_code: true,
memoize_properties: true,
allow_possibly_undefined_array_keys: true,
..Default::default()
}
}
#[must_use]
pub fn no_boolean_literal_comparison_settings() -> Settings {
Settings { no_boolean_literal_comparison: true, ..Default::default() }
}
#[must_use]
pub fn php_90_settings() -> Settings {
Settings::new(mago_php_version::PHPVersion::new(9, 0, 0))
}
#[must_use]
pub fn check_name_casing_settings() -> Settings {
Settings { check_name_casing: true, ..Default::default() }
}
#[must_use]
pub fn check_missing_type_hints_settings() -> Settings {
Settings { check_missing_type_hints: 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.as_bytes().to_vec()), Cow::Owned(config.content.to_vec()));
let file_id = database.add(file);
let source_file = database.get_ref(&file_id).expect("File just added should exist");
let arena = LocalArena::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);
let settings = config.settings.unwrap_or_else(default_test_settings);
metadata.extend(scan_program(&arena, source_file, program, &resolved_names, settings.version));
populate_codebase(&mut metadata, &mut symbol_references, WordSet::default(), HashSet::default());
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 {
let _ = writeln!(panic_message, " {d}");
}
panic!("{}", panic_message);
}
}