mod common;
use mir_analyzer::{AnalysisSession, BatchOptions, PhpVersion};
use self::common::{create_temp_dir, write_file};
fn type_check_mismatches(result: &mir_analyzer::AnalysisResult) -> usize {
result
.issues
.iter()
.filter(|i| i.kind.name() == "TypeCheckMismatch")
.count()
}
#[test]
fn transitive_inferred_return_invalidation() {
let src_dir = create_temp_dir("inferred_invalidation: source");
let cache_dir = create_temp_dir("inferred_invalidation: cache");
let c = write_file(
&src_dir,
"C.php",
"<?php\nfunction c_val() {\n return 42;\n}\n",
);
let b = write_file(
&src_dir,
"B.php",
"<?php\nfunction b_val() {\n return c_val();\n}\n",
);
let a = write_file(
&src_dir,
"A.php",
"<?php\nfunction a_test(): void {\n $x = b_val();\n /** @mir-check $x is int */\n echo $x;\n}\n",
);
let files = [a.clone(), b.clone(), c.clone()];
let session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let result1 = session.analyze_paths(&files, &BatchOptions::new());
assert_eq!(
type_check_mismatches(&result1),
0,
"run 1: cross-file inferred return should resolve to int (got mismatches: {:#?})",
result1
.issues
.iter()
.filter(|i| i.kind.name() == "TypeCheckMismatch")
.collect::<Vec<_>>()
);
write_file(
&src_dir,
"C.php",
"<?php\nfunction c_val() {\n return \"str\";\n}\n",
);
let session2 = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let result2 = session2.analyze_paths(&files, &BatchOptions::new());
assert_eq!(
type_check_mismatches(&result2),
1,
"run 2: editing C changes b_val()'s inferred return to string; A must be \
re-analyzed and report the @mir-check mismatch. A count of 0 means A was \
served a STALE cache hit (reverse-dep graph missed the inferred edge)."
);
}
#[test]
fn transitive_inferred_return_invalidation_via_methods() {
let src_dir = create_temp_dir("inferred_invalidation_methods: source");
let cache_dir = create_temp_dir("inferred_invalidation_methods: cache");
let c = write_file(
&src_dir,
"C.php",
"<?php\nclass C {\n public function val() {\n return 42;\n }\n}\n",
);
let b = write_file(
&src_dir,
"B.php",
"<?php\nclass B {\n public function val() {\n return (new C())->val();\n }\n}\n",
);
let a = write_file(
&src_dir,
"A.php",
"<?php\nclass A {\n public function test(): void {\n $x = (new B())->val();\n /** @mir-check $x is int */\n echo $x;\n }\n}\n",
);
let files = [a.clone(), b.clone(), c.clone()];
let session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let result1 = session.analyze_paths(&files, &BatchOptions::new());
assert_eq!(
type_check_mismatches(&result1),
0,
"run 1 (methods): cross-file inferred method return should resolve to int"
);
write_file(
&src_dir,
"C.php",
"<?php\nclass C {\n public function val() {\n return \"str\";\n }\n}\n",
);
let session2 = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let result2 = session2.analyze_paths(&files, &BatchOptions::new());
assert_eq!(
type_check_mismatches(&result2),
1,
"run 2 (methods): editing C must re-analyze A via the structural `new C()` edge"
);
}
#[test]
fn transitive_inferred_return_invalidation_via_trait() {
let src_dir = create_temp_dir("inferred_invalidation_trait: source");
let cache_dir = create_temp_dir("inferred_invalidation_trait: cache");
let c = write_file(
&src_dir,
"C.php",
"<?php\nfunction c_v() {\n return 42;\n}\n",
);
let t = write_file(
&src_dir,
"T.php",
"<?php\ntrait T {\n public function tv() {\n return c_v();\n }\n}\n",
);
let a = write_file(
&src_dir,
"A.php",
"<?php\nclass A {\n use T;\n public function test(): void {\n $x = $this->tv();\n /** @mir-check $x is int */\n echo $x;\n }\n}\n",
);
let files = [a.clone(), t.clone(), c.clone()];
let session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let result1 = session.analyze_paths(&files, &BatchOptions::new());
assert_eq!(
type_check_mismatches(&result1),
0,
"run 1 (trait): inferred return through a trait method should resolve to int"
);
write_file(
&src_dir,
"C.php",
"<?php\nfunction c_v() {\n return \"str\";\n}\n",
);
let session2 = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let result2 = session2.analyze_paths(&files, &BatchOptions::new());
assert_eq!(
type_check_mismatches(&result2),
1,
"run 2 (trait): editing C must re-analyze A through the T trait + c_v() edges"
);
}
#[test]
fn deleting_a_dependency_file_invalidates_dependents() {
let src_dir = create_temp_dir("delete_dependency: source");
let cache_dir = create_temp_dir("delete_dependency: cache");
let c = write_file(
&src_dir,
"C.php",
"<?php\nfunction c_val() {\n return 42;\n}\n",
);
let a = write_file(
&src_dir,
"A.php",
"<?php\nfunction a_test(): void {\n $x = c_val();\n /** @mir-check $x is int */\n echo $x;\n}\n",
);
{
let baseline_cache = create_temp_dir("delete_dependency: baseline cache");
let baseline = AnalysisSession::new(PhpVersion::LATEST)
.with_cache_dir(baseline_cache.path())
.analyze_paths(std::slice::from_ref(&a), &BatchOptions::new());
assert!(
type_check_mismatches(&baseline) >= 1,
"baseline: with C absent, c_val() is unresolved and the @mir-check must \
flag a mismatch (signal sanity check)"
);
}
let session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let result1 = session.analyze_paths(&[a.clone(), c.clone()], &BatchOptions::new());
assert_eq!(
type_check_mismatches(&result1),
0,
"run 1: C present, $x resolves to int"
);
std::fs::remove_file(&c).unwrap();
let session2 = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(cache_dir.path());
let result2 = session2.analyze_paths(std::slice::from_ref(&a), &BatchOptions::new());
assert_eq!(
type_check_mismatches(&result2),
1,
"run 2: C was deleted; A must be re-analyzed. A count of 0 means A was \
served a STALE cache hit (deleted dependency not invalidated)."
);
}