mod common;
use std::fs;
use std::sync::Arc;
use mir_analyzer::{AnalysisSession, FileAnalyzer, IndexCancel, PhpVersion};
use self::common::create_temp_dir;
fn write_fixture(root: &std::path::Path) {
fs::write(
root.join("composer.json"),
r#"{
"autoload": {
"psr-4": {
"App\\": "src/",
"Vendor\\": "vendor/VendorLib/src/"
}
}
}"#,
)
.unwrap();
fs::create_dir_all(root.join("src")).unwrap();
fs::create_dir_all(root.join("vendor/VendorLib/src")).unwrap();
fs::write(
root.join("vendor/VendorLib/src/Dep.php"),
"<?php\nnamespace Vendor;\nclass Dep { public function run(): int { return 1; } }\n",
)
.unwrap();
fs::write(
root.join("src/Consumer.php"),
"<?php\nnamespace App;\nuse Vendor\\Dep;\nclass Consumer { public function go(Dep $d): int { return $d->run(); } }\n",
)
.unwrap();
}
fn make_session(root: &std::path::Path) -> AnalysisSession {
let psr4 = mir_analyzer::composer::Psr4Map::from_composer(root).expect("psr4 map");
AnalysisSession::new(PhpVersion::LATEST).with_psr4(Arc::new(psr4))
}
#[test]
fn ingest_file_prepared_faults_in_direct_references_at_write_time() {
let dir = create_temp_dir("hoist_prepared");
let root = dir.path();
write_fixture(root);
let consumer: Arc<str> = Arc::from(root.join("src/Consumer.php").to_string_lossy().as_ref());
let src: Arc<str> = Arc::from(fs::read_to_string(consumer.as_ref()).unwrap().as_str());
let session = make_session(root);
session.ingest_file(consumer.clone(), src.clone());
assert!(
!session.contains_class("Vendor\\Dep"),
"plain ingest_file must not lazy-load referenced classes"
);
let session = make_session(root);
session.ingest_file_prepared(consumer.clone(), src.clone());
assert!(
session.contains_class("Vendor\\Dep"),
"ingest_file_prepared must lazy-load the file's direct references"
);
drop(dir);
}
#[test]
fn no_ref_index_locks_on_edit_or_read_paths_when_opted_out() {
let file_a: Arc<str> = Arc::from("hoist_a.php");
let file_b: Arc<str> = Arc::from("hoist_b.php");
let src_a = "<?php\nclass HoistBase { public function m(): int { return 1; } }\n";
let src_b = "<?php\nclass HoistDep extends HoistBase {}\nfunction hb(): int { $x = new HoistBase(); return $x->m(); }\n";
let run_flows = |session: &AnalysisSession| {
session.ensure_all_stubs();
session.ingest_file(file_a.clone(), Arc::from(src_a));
session.ingest_file(file_a.clone(), Arc::from(src_a));
session.ingest_file_prepared(file_b.clone(), Arc::from(src_b));
let parsed = php_rs_parser::parse(src_a);
let _ = FileAnalyzer::new(session).analyze(
file_a.clone(),
src_a,
&parsed.program,
&parsed.source_map,
);
let refs = session.references_to_in_files(
&mir_analyzer::Name::method("HoistBase", "m"),
&[file_a.clone(), file_b.clone()],
);
assert!(!refs.is_empty(), "pure references path must still work");
let _ =
session.reanalyze_files_cancellable(std::slice::from_ref(&file_b), &IndexCancel::new());
session.invalidate_file(file_b.as_ref());
};
let cache_dir = create_temp_dir("hoist_locks");
let session = AnalysisSession::new(PhpVersion::LATEST)
.with_cache_dir(cache_dir.path())
.without_reference_index();
run_flows(&session);
assert_eq!(
session.ref_index_lock_count(),
0,
"RefIndex was locked on an edit/read path despite without_reference_index()"
);
let control = AnalysisSession::new(PhpVersion::LATEST);
run_flows(&control);
assert!(
control.ref_index_lock_count() > 0,
"control session should lock RefIndex; the counter is vacuous"
);
}