use std::sync::Arc;
use crate::db::{analyze_file_per_scope, MirDatabase, RefLoc};
use crate::{AnalysisSession, BatchOptions, PhpVersion};
fn assert_scope_parity(name: &str, source: &str) {
let mut session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(format!("{name}.php"));
std::fs::write(&path, source).unwrap();
let _ = session.analyze_paths(
std::slice::from_ref(&path),
&BatchOptions::new().without_symbols(),
);
let db = session.snapshot_db();
let path_str: Arc<str> = Arc::from(path.to_string_lossy().as_ref());
let file = db.lookup_source_file(path_str.as_ref()).unwrap();
let parsed = crate::db::parse_file(&db, file);
let expected_issues = {
let mut analyzer = crate::FileAnalyzer::new(&mut session);
let analysis = analyzer.analyze_diagnostics_only(
path_str.clone(),
source,
&parsed.0.program,
&parsed.0.source_map,
);
analysis.issues
};
let mut expected_refs: Vec<RefLoc> = db
.extract_file_reference_locations(path_str.as_ref())
.into_iter()
.map(|(symbol_key, line, col_start, col_end)| RefLoc {
symbol_key,
file: path_str.clone(),
line,
col_start,
col_end,
})
.collect();
expected_refs.sort();
expected_refs.dedup();
let (scope_issues, mut scope_refs) = analyze_file_per_scope(&db, file);
scope_refs.sort();
scope_refs.dedup();
assert_eq!(
expected_issues, scope_issues,
"issue stream mismatch (order included) for fixture `{name}`"
);
for r in &scope_refs {
assert!(
expected_refs.contains(r),
"per-scope ref {r:?} missing from whole-file set for `{name}`"
);
}
for r in &expected_refs {
assert!(
scope_refs.contains(r),
"whole-file ref {r:?} missing from per-scope set for `{name}`"
);
}
}
#[test]
fn parity_functions_and_classes() {
assert_scope_parity(
"mixed",
r#"<?php
use Foo\Bar;
function plain(): string {
return "hello";
}
class Greeter {
public string $name;
private $untypedProp;
public function greet(int $times): string {
$out = "";
for ($i = 0; $i < $times; $i++) {
$out .= "hi";
}
return $out;
}
public function broken(): int {
return $undefined_var;
}
}
function returns_str(): string {
return 42;
}
"#,
);
}
#[test]
fn parity_trait_and_enum() {
assert_scope_parity(
"trait_enum",
r#"<?php
trait Helper {
public function help(): string {
return "help";
}
}
enum Suit: string {
case Hearts = 'H';
case Spades = 'S';
public function color(): string {
return match($this) {
Suit::Hearts => 'Red',
Suit::Spades => 'Black',
};
}
}
interface Shape {
public function area(): float;
}
"#,
);
}
#[test]
fn parity_namespaced_and_guarded() {
assert_scope_parity(
"ns_guarded",
r#"<?php
namespace App;
class Service {
public function run(): void {
$unused = 1;
}
}
if (! function_exists('App\helper')) {
function helper(): string {
return 42;
}
}
$x = new Service();
$x->run();
"#,
);
}
#[test]
fn parity_duplicate_declarations() {
assert_scope_parity(
"dups",
r#"<?php
function twice(): string {
return "a";
}
function twice(): string {
return 42;
}
"#,
);
}
#[test]
fn parity_top_level_exec_only() {
assert_scope_parity(
"exec",
r#"<?php
$a = 1;
$b = $a + $undefined_thing;
echo $b;
"#,
);
}
#[test]
fn parity_class_exists_guarded_declarations() {
assert_scope_parity(
"class_exists_guard",
r#"<?php
namespace App;
use Vendor\Missing\ClientInterface;
use Vendor\Missing\Plugin;
if (!interface_exists(ClientInterface::class)) {
throw new \LogicException('install vendor/missing');
}
final class Client implements ClientInterface {
}
if (!interface_exists(Plugin::class)) {
throw new \LogicException('install vendor/missing');
}
interface Extension extends Plugin {
}
"#,
);
}