mod common;
use std::path::PathBuf;
use mir_analyzer::{AnalysisSession, PhpVersion, ProjectAnalyzer, Symbol};
use self::common::{create_temp_dir, write_file};
fn write_fixture(src_dir: &tempfile::TempDir) -> (PathBuf, PathBuf) {
let a = write_file(
src_dir,
"A.php",
"<?php\n\
namespace App;\n\
class A {\n\
public function greet(string $name): string { return \"hi $name\"; }\n\
}\n",
);
let b = write_file(
src_dir,
"B.php",
"<?php\n\
namespace App;\n\
class B {\n\
public function run(A $a): string { return $a->greet('mir'); }\n\
}\n",
);
(a, b)
}
#[test]
fn project_analyzer_cold_and_warm_produce_identical_symbol_table() {
let src_dir = create_temp_dir("stub_cache_correctness: src");
let cache_dir = create_temp_dir("stub_cache_correctness: cache");
let (a, b) = write_fixture(&src_dir);
let paths = [a.clone(), b.clone()];
let cold = ProjectAnalyzer::with_cache(cache_dir.path());
let cold_result = cold.analyze(&paths);
let cold_issues = cold_result.issues.len();
assert!(
cold.contains_class("App\\A"),
"App\\A should be registered after cold run"
);
assert!(
cold.contains_method("App\\A", "greet"),
"App\\A::greet should be visible after cold run"
);
let (cold_hits, _cold_misses) = cold.stub_cache_stats();
drop(cold);
let warm = ProjectAnalyzer::with_cache(cache_dir.path());
let warm_result = warm.analyze(&paths);
assert_eq!(
warm_result.issues.len(),
cold_issues,
"warm run must produce the same issues as cold"
);
assert!(warm.contains_class("App\\A"));
assert!(warm.contains_method("App\\A", "greet"));
let (_warm_hits, _warm_misses) = warm.stub_cache_stats();
let _ = cold_hits;
}
#[test]
fn analysis_session_warm_cache_observes_hits_and_preserves_symbols() {
let src_dir = create_temp_dir("stub_cache_correctness: lsp src");
let cache_dir = create_temp_dir("stub_cache_correctness: lsp cache");
let (a, b) = write_fixture(&src_dir);
let a_path: std::sync::Arc<str> = std::sync::Arc::from(a.to_string_lossy().as_ref());
let b_path: std::sync::Arc<str> = std::sync::Arc::from(b.to_string_lossy().as_ref());
let a_src: std::sync::Arc<str> =
std::sync::Arc::from(std::fs::read_to_string(&a).unwrap().as_str());
let b_src: std::sync::Arc<str> =
std::sync::Arc::from(std::fs::read_to_string(&b).unwrap().as_str());
{
let session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
session.ensure_essential_stubs_loaded();
session.ingest_file(a_path.clone(), a_src.clone());
session.ingest_file(b_path.clone(), b_src.clone());
let def = session
.definition_of(&Symbol::class("App\\A"))
.expect("App\\A defined in cold session");
assert_eq!(def.file.as_ref(), a_path.as_ref());
}
let session2 = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
session2.ensure_essential_stubs_loaded();
session2.ingest_file(a_path.clone(), a_src.clone());
session2.ingest_file(b_path.clone(), b_src.clone());
let def = session2
.definition_of(&Symbol::class("App\\A"))
.expect("App\\A must still be defined in warm session");
assert_eq!(def.file.as_ref(), a_path.as_ref());
let def_b = session2
.definition_of(&Symbol::class("App\\B"))
.expect("App\\B must be defined in warm session");
assert_eq!(def_b.file.as_ref(), b_path.as_ref());
}
#[test]
fn cache_miss_after_content_change() {
let src_dir = create_temp_dir("stub_cache_correctness: invalidation");
let cache_dir = create_temp_dir("stub_cache_correctness: cache");
let a_path = write_file(
&src_dir,
"A.php",
"<?php\nnamespace App; class A { public function v1(): void {} }\n",
);
let a_arc: std::sync::Arc<str> = std::sync::Arc::from(a_path.to_string_lossy().as_ref());
let v1: std::sync::Arc<str> =
std::sync::Arc::from(std::fs::read_to_string(&a_path).unwrap().as_str());
{
let session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
session.ensure_essential_stubs_loaded();
session.ingest_file(a_arc.clone(), v1);
assert!(session
.definition_of(&Symbol::method("App\\A", "v1"))
.is_ok());
}
write_file(
&src_dir,
"A.php",
"<?php\nnamespace App; class A { public function v2(): void {} }\n",
);
let v2: std::sync::Arc<str> =
std::sync::Arc::from(std::fs::read_to_string(&a_path).unwrap().as_str());
let session2 = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
session2.ensure_essential_stubs_loaded();
session2.ingest_file(a_arc.clone(), v2);
assert!(
session2
.definition_of(&Symbol::method("App\\A", "v2"))
.is_ok(),
"renamed method v2 must appear after content change"
);
assert!(
session2
.definition_of(&Symbol::method("App\\A", "v1"))
.is_err(),
"old method v1 must not survive a content change"
);
}