mod common;
use mir_analyzer::{dead_code_issue_kinds, AnalysisSession, BatchOptions, PhpVersion};
use self::common::{create_temp_dir, write_file};
#[test]
fn dependent_file_is_reanalyzed_when_base_changes() {
let src_dir = create_temp_dir("cache_invalidation: source files");
let cache_dir = create_temp_dir("cache_invalidation: cache");
let base = write_file(
&src_dir,
"Base.php",
"<?php\nclass Base {\n public function foo(): void {}\n}\n",
);
let child = write_file(
&src_dir,
"Child.php",
"<?php\nclass Child extends Base {}\nfunction test(): void {\n $c = new Child();\n $c->foo();\n}\n",
);
let session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let result1 = session.analyze_paths(&[base.clone(), child.clone()], &BatchOptions::new());
let undefined_method_count = result1
.issues
.iter()
.filter(|i| i.kind.name() == "UndefinedMethod")
.count();
assert_eq!(undefined_method_count, 0, "first run: no issues expected");
write_file(
&src_dir,
"Base.php",
"<?php\nclass Base {\n // foo() removed\n}\n",
);
let session2 = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let result2 = session2.analyze_paths(&[base.clone(), child.clone()], &BatchOptions::new());
let undefined_method_count2 = result2
.issues
.iter()
.filter(|i| i.kind.name() == "UndefinedMethod")
.count();
assert_eq!(
undefined_method_count2, 1,
"second run: Child must be re-analyzed and report UndefinedMethod for foo()"
);
}
#[test]
fn unrelated_file_cache_entry_survives() {
let src_dir = create_temp_dir("unrelated_file: source files");
let cache_dir = create_temp_dir("unrelated_file: cache");
let base = write_file(
&src_dir,
"Base.php",
"<?php\nclass Base {\n public function foo(): void {}\n}\n",
);
let unrelated = write_file(
&src_dir,
"Unrelated.php",
"<?php\nfunction helper(): void {}\n",
);
let session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let opts = BatchOptions::new().with_suppressed(dead_code_issue_kinds().iter().copied());
session.analyze_paths(&[base.clone(), unrelated.clone()], &opts);
write_file(
&src_dir,
"Base.php",
"<?php\nclass Base {\n public function bar(): void {}\n}\n",
);
let session2 = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let opts2 = BatchOptions::new().with_suppressed(dead_code_issue_kinds().iter().copied());
let result = session2.analyze_paths(&[base.clone(), unrelated.clone()], &opts2);
let unrelated_str = unrelated.to_string_lossy();
let issues_for_unrelated: Vec<_> = result
.issues
.iter()
.filter(|i| i.location.file.as_ref() == unrelated_str.as_ref())
.collect();
assert!(
issues_for_unrelated.is_empty(),
"unrelated file should produce no issues: {issues_for_unrelated:?}"
);
}
fn reanalyzed_count(cache_dir: &std::path::Path, paths: &[std::path::PathBuf]) -> usize {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let n = Arc::new(AtomicUsize::new(0));
let counter = n.clone();
let opts = BatchOptions::new().with_progress_callback(Arc::new(move || {
counter.fetch_add(1, Ordering::Relaxed);
}));
let session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir);
session.analyze_paths(paths, &opts);
n.load(Ordering::Relaxed)
}
#[test]
fn body_only_change_to_base_does_not_reanalyze_dependent() {
let src_dir = create_temp_dir("firewall: src");
let cache_dir = create_temp_dir("firewall: cache");
let base = write_file(
&src_dir,
"Base.php",
"<?php\nclass Base { public function foo(): int { return 1; } }\n",
);
let child = write_file(
&src_dir,
"Child.php",
"<?php\nclass Child extends Base {\n public function bar(): int { return $this->foo(); }\n}\n",
);
let session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
session.analyze_paths(&[base.clone(), child.clone()], &BatchOptions::new());
write_file(
&src_dir,
"Base.php",
"<?php\nclass Base { public function foo(): int { $x = 41; return $x + 1; } }\n",
);
let reanalyzed = reanalyzed_count(cache_dir.path(), &[base.clone(), child.clone()]);
assert_eq!(
reanalyzed, 1,
"only Base should be re-analyzed; Child's cached result must survive a body-only change"
);
}
#[test]
fn signature_change_to_base_reanalyzes_dependent() {
let src_dir = create_temp_dir("firewall_control: src");
let cache_dir = create_temp_dir("firewall_control: cache");
let base = write_file(
&src_dir,
"Base.php",
"<?php\nclass Base { public function foo(): int { return 1; } }\n",
);
let child = write_file(
&src_dir,
"Child.php",
"<?php\nclass Child extends Base {\n public function bar(): int { return $this->foo(); }\n}\n",
);
let session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
session.analyze_paths(&[base.clone(), child.clone()], &BatchOptions::new());
write_file(
&src_dir,
"Base.php",
"<?php\nclass Base { public function foo(): string { return 'x'; } }\n",
);
let reanalyzed = reanalyzed_count(cache_dir.path(), &[base.clone(), child.clone()]);
assert_eq!(
reanalyzed, 2,
"a signature change to Base must cascade re-analysis to Child"
);
}
#[test]
fn warm_run_without_changes_does_not_rewrite_cache() {
let src_dir = create_temp_dir("warm_run: source files");
let cache_dir = create_temp_dir("warm_run: cache");
let base = write_file(
&src_dir,
"Base.php",
"<?php\nclass Base {\n public function foo(): void {}\n}\n",
);
let child = write_file(
&src_dir,
"Child.php",
"<?php\nclass Child extends Base {}\nfunction test(): void {\n (new Child())->foo();\n}\n",
);
let session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let result1 = session.analyze_paths(&[base.clone(), child.clone()], &BatchOptions::new());
let cache_bin = cache_dir.path().join("cache.bin");
let mtime1 = std::fs::metadata(&cache_bin)
.expect("cache.bin should exist after first run")
.modified()
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
let session2 = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let result2 = session2.analyze_paths(&[base.clone(), child.clone()], &BatchOptions::new());
let mtime2 = std::fs::metadata(&cache_bin).unwrap().modified().unwrap();
assert_eq!(
mtime1, mtime2,
"an unchanged warm run must not rewrite cache.bin"
);
assert_eq!(
result1.issues.len(),
result2.issues.len(),
"warm run must produce the same diagnostics"
);
}