use std::sync::Arc;
use mir_analyzer::{AnalysisSession, Name, PhpVersion, SymbolLookupError};
#[test]
fn hover_returns_real_info_for_function() {
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let file: Arc<str> = Arc::from("test.php");
let source: Arc<str> = Arc::from(
"<?php\n\
/**\n\
* Adds two integers and returns the sum.\n\
*/\n\
function add(int $a, int $b): int { return $a + $b; }\n",
);
session.ingest_file(file.clone(), source.clone());
let hover = session
.hover(&Name::function("add"))
.expect("add() should be resolvable");
assert!(
hover.docstring.is_some(),
"Docstring should be populated from the docblock description"
);
assert!(
hover
.docstring
.as_ref()
.unwrap()
.contains("Adds two integers"),
"Docstring should include the description text, got: {:?}",
hover.docstring
);
assert!(
hover.definition.is_some(),
"Function should have a source location"
);
}
#[test]
fn hover_returns_not_found_for_unknown_symbol() {
let session = AnalysisSession::new(PhpVersion::LATEST);
let result = session.hover(&Name::function("nonexistent_function_xyz"));
assert_eq!(result.unwrap_err(), SymbolLookupError::NotFound);
}
#[test]
fn symbol_method_normalizes_case() {
let s1 = Name::method("Foo", "Bar");
let s2 = Name::method("Foo", "bar");
let s3 = Name::method("Foo", "BAR");
assert_eq!(s1, s2);
assert_eq!(s1, s3);
assert_eq!(s1.codebase_key(), "Foo::bar");
}
#[test]
fn definition_of_returns_result_with_distinct_errors() {
let session = AnalysisSession::new(PhpVersion::LATEST);
let err = session
.definition_of(&Name::class("CompletelyMadeUp"))
.unwrap_err();
assert_eq!(err, SymbolLookupError::NotFound);
}
#[test]
fn document_symbols_returns_hierarchical_tree() {
use mir_analyzer::symbol::DeclarationKind;
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let file: Arc<str> = Arc::from("hierarchy.php");
let source: Arc<str> = Arc::from(
"<?php\n\
class Container {\n\
public int $count = 0;\n\
const VERSION = 1;\n\
public function add(int $n): void {}\n\
public function reset(): void {}\n\
}\n",
);
session.ingest_file(file.clone(), source.clone());
let symbols = session.document_symbols(file.as_ref());
let container = symbols
.iter()
.find(|s| s.name.as_ref() == "Container")
.expect("Container class should be in document symbols");
assert_eq!(container.kind, DeclarationKind::Class);
assert!(
!container.children.is_empty(),
"Class should have children (methods, properties, constants)"
);
let kinds: Vec<DeclarationKind> = container.children.iter().map(|c| c.kind).collect();
assert!(
kinds.contains(&DeclarationKind::Method),
"Should have at least one method child, got: {kinds:?}"
);
}
#[test]
fn references_to_takes_typed_symbol() {
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let file: Arc<str> = Arc::from("refs.php");
let source: Arc<str> = Arc::from(
"<?php\n\
function helper(): void {}\n\
function caller(): void { helper(); helper(); }\n",
);
session.ingest_file(file.clone(), source.clone());
use mir_analyzer::FileAnalyzer;
let parsed = php_rs_parser::parse(&source);
let _analysis = FileAnalyzer::new(&session).analyze(
file.clone(),
&source,
&parsed.program,
&parsed.source_map,
);
let refs = session.references_to(&Name::function("helper"));
assert!(
refs.iter().any(|(f, _)| f.as_ref() == file.as_ref()),
"Should find references to helper in {}",
file
);
}
#[test]
fn analysis_session_builder_pattern() {
use mir_analyzer::{AnalysisSession, BatchOptions, PhpVersion};
let _session = AnalysisSession::new(PhpVersion::LATEST);
let mut opts =
BatchOptions::new().with_suppressed(mir_analyzer::dead_code_issue_kinds().iter().copied());
for kind in mir_analyzer::dead_code_issue_kinds() {
opts.suppressed_issue_kinds.remove(*kind);
}
}
#[test]
fn analysis_session_with_cache_dir() {
let temp = std::env::temp_dir().join("mir_test_cache_xyz");
let _session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(&temp);
let _ = std::fs::remove_dir_all(&temp);
}
#[test]
fn symbol_kind_variable_uses_arc_str() {
use mir_analyzer::symbol::ReferenceKind;
let kind = ReferenceKind::Variable(Arc::from("count"));
match kind {
ReferenceKind::Variable(name) => {
assert_eq!(name.as_ref(), "count");
}
_ => panic!("expected Variable"),
}
}
#[test]
fn re_exports_available_at_crate_root() {
let _: mir_analyzer::Visibility = mir_analyzer::Visibility::Public;
let _name: &'static str = std::any::type_name::<mir_analyzer::FnParam>();
let _name: &'static str = std::any::type_name::<mir_analyzer::TemplateParam>();
}
#[test]
fn contains_function_class_method_typed_queries() {
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ingest_file(
Arc::from("typed.php"),
Arc::from(
"<?php\n\
class Worker { public function run(): void {} }\n\
function helper(): void {}\n",
),
);
assert!(session.contains_class("Worker"));
assert!(session.contains_function("helper"));
assert!(session.contains_method("Worker", "run"));
assert!(session.contains_method("Worker", "RUN"));
assert!(session.contains_method("Worker", "Run"));
assert!(!session.contains_class("DoesNotExist"));
assert!(!session.contains_function("does_not_exist_xyz"));
assert!(!session.contains_method("Worker", "missing"));
}
#[test]
fn resolved_symbol_to_symbol_bridges_pass2_with_queries() {
use mir_analyzer::symbol::ReferenceKind;
use mir_analyzer::FileAnalyzer;
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let file: Arc<str> = Arc::from("bridge.php");
let source: Arc<str> = Arc::from(
"<?php\n\
function helper(): void {}\n\
function caller(): void { helper(); }\n",
);
session.ingest_file(file.clone(), source.clone());
let parsed = php_rs_parser::parse(&source);
let analysis = FileAnalyzer::new(&session).analyze(
file.clone(),
&source,
&parsed.program,
&parsed.source_map,
);
let helper_call = analysis
.symbols
.iter()
.find(|s| matches!(&s.kind, ReferenceKind::FunctionCall(name) if name.as_ref() == "helper"))
.expect("should record helper() call in caller body");
let typed_symbol = helper_call
.to_symbol()
.expect("FunctionCall should convert to Name");
assert_eq!(typed_symbol, Name::function("helper"));
let refs = session.references_to(&typed_symbol);
assert!(refs.iter().any(|(f, _)| f.as_ref() == file.as_ref()));
}
#[test]
fn method_references_scoped_by_declaring_class() {
use mir_analyzer::FileAnalyzer;
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let file: Arc<str> = Arc::from("scope.php");
let source: Arc<str> = Arc::from(
"<?php\n\
final class Foo { public function toString(): string { return 'foo'; } }\n\
final class Bar { public function toString(): string { return 'bar'; } }\n\
(new Foo())->toString();\n",
);
session.ingest_file(file.clone(), source.clone());
let parsed = php_rs_parser::parse(&source);
let _ = FileAnalyzer::new(&session).analyze(
file.clone(),
&source,
&parsed.program,
&parsed.source_map,
);
let foo_refs = session.references_to(&Name::method("Foo", "toString"));
let bar_refs = session.references_to(&Name::method("Bar", "toString"));
assert!(
!foo_refs.is_empty(),
"Foo::toString should have at least one reference (the call site); got none"
);
assert!(
bar_refs.is_empty(),
"Bar::toString should have zero references; got {bar_refs:?}"
);
let foo_lines: Vec<u32> = foo_refs.iter().map(|(_, r)| r.start.line).collect();
assert!(
foo_lines.contains(&4),
"Expected reference on line 4 (1-based); got {foo_lines:?}"
);
}
#[test]
fn method_references_end_to_end_symbol_at_flow() {
use mir_analyzer::symbol::ReferenceKind;
use mir_analyzer::FileAnalyzer;
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let file: Arc<str> = Arc::from("e2e.php");
let source: Arc<str> = Arc::from(
"<?php\n\
final class Foo { public function toString(): string { return 'foo'; } }\n\
(new Foo())->toString();\n",
);
session.ingest_file(file.clone(), source.clone());
let parsed = php_rs_parser::parse(&source);
let analysis = FileAnalyzer::new(&session).analyze(
file.clone(),
&source,
&parsed.program,
&parsed.source_map,
);
let call_offset = source.find("->toString").unwrap() as u32 + 2;
let sym = analysis
.symbol_at(call_offset)
.expect("should resolve symbol at toString call site");
assert!(
matches!(&sym.kind, ReferenceKind::MethodCall { class, .. } if class.as_ref() == "Foo"),
"symbol_at should report class Foo; got {:?}",
sym.kind
);
let name = sym.to_symbol().expect("MethodCall should map to a Name");
let refs = session.references_to(&name);
assert!(
!refs.is_empty(),
"references_to via symbol_at flow must find the call site; got none"
);
}
#[test]
fn method_references_inherited_method_end_to_end() {
use mir_analyzer::symbol::ReferenceKind;
use mir_analyzer::FileAnalyzer;
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let file: Arc<str> = Arc::from("inherit.php");
let source: Arc<str> = Arc::from(
"<?php\n\
class Base { public function toString(): string { return 'b'; } }\n\
final class Foo extends Base {}\n\
(new Foo())->toString();\n",
);
session.ingest_file(file.clone(), source.clone());
let parsed = php_rs_parser::parse(&source);
let analysis = FileAnalyzer::new(&session).analyze(
file.clone(),
&source,
&parsed.program,
&parsed.source_map,
);
let call_offset = source.find("->toString").unwrap() as u32 + 2;
let sym = analysis
.symbol_at(call_offset)
.expect("should resolve symbol at toString call");
let declaring_class = match &sym.kind {
ReferenceKind::MethodCall { class, .. } => class.as_ref().to_string(),
other => panic!("unexpected kind: {other:?}"),
};
assert_eq!(
declaring_class, "Base",
"symbol_at must report the DECLARING class (Base), not the receiver (Foo)"
);
let name = sym.to_symbol().expect("MethodCall maps to Name");
let refs = session.references_to(&name);
assert!(
!refs.is_empty(),
"references_to(Base::toString) must find the (new Foo())->toString() call; \
got none (declaring_class was '{declaring_class}', refs: {refs:?})"
);
}
#[test]
fn load_class_with_custom_resolver() {
use mir_analyzer::{ClassResolver, LoadOutcome};
use std::path::PathBuf;
struct TmpResolver {
path: PathBuf,
}
impl ClassResolver for TmpResolver {
fn resolve(&self, _fqcn: &str) -> Option<PathBuf> {
Some(self.path.clone())
}
}
let dir = std::env::temp_dir().join(format!("mir_lazy_test_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file_path = dir.join("Resolved.php");
std::fs::write(&file_path, "<?php\nclass ResolvedByCustom {}\n").unwrap();
let resolver: Arc<dyn ClassResolver> = Arc::new(TmpResolver {
path: file_path.clone(),
});
let session = AnalysisSession::new(PhpVersion::LATEST).with_class_resolver(resolver);
assert!(!session.contains_class("ResolvedByCustom"));
let outcome = session.load_class("ResolvedByCustom");
assert_eq!(outcome, LoadOutcome::Loaded);
assert!(session.contains_class("ResolvedByCustom"));
let outcome = session.load_class("ResolvedByCustom");
assert_eq!(outcome, LoadOutcome::AlreadyLoaded);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn prefetch_imports_loads_unresolved_use_statements() {
use mir_analyzer::ClassResolver;
use std::path::PathBuf;
use std::sync::Mutex;
struct TrackedResolver {
map: std::collections::HashMap<String, PathBuf>,
calls: Mutex<Vec<String>>,
}
impl ClassResolver for TrackedResolver {
fn resolve(&self, fqcn: &str) -> Option<PathBuf> {
self.calls.lock().unwrap().push(fqcn.to_string());
self.map.get(fqcn).cloned()
}
}
let dir = std::env::temp_dir().join(format!("mir_prefetch_test_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let dep_path = dir.join("Dep.php");
std::fs::write(&dep_path, "<?php\nnamespace App;\nclass Dep {}\n").unwrap();
let mut map = std::collections::HashMap::new();
map.insert("App\\Dep".to_string(), dep_path.clone());
let resolver = Arc::new(TrackedResolver {
map,
calls: Mutex::new(Vec::new()),
});
let session = AnalysisSession::new(PhpVersion::LATEST).with_class_resolver(resolver.clone());
let opened: Arc<str> = Arc::from("opened.php");
let opened_src: Arc<str> =
Arc::from("<?php\nuse App\\Dep;\nclass Caller { public function go(Dep $d): void {} }\n");
session.ingest_file(opened.clone(), opened_src);
assert!(!session.contains_class("App\\Dep"));
let pending = session.pending_lazy_loads(opened.as_ref());
assert!(
pending.iter().any(|s| s.as_ref() == "App\\Dep"),
"pending should include App\\Dep, got {:?}",
pending
);
let loaded = session.prefetch_imports(opened.as_ref());
assert!(loaded >= 1, "prefetch should load at least App\\Dep");
assert!(session.contains_class("App\\Dep"));
assert_eq!(session.pending_lazy_loads(opened.as_ref()).len(), 0);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn reanalyze_dependents_runs_in_parallel() {
use mir_analyzer::FileAnalyzer;
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let base: Arc<str> = Arc::from("base.php");
let dep_a: Arc<str> = Arc::from("dep_a.php");
let dep_b: Arc<str> = Arc::from("dep_b.php");
session.ingest_file(base.clone(), Arc::from("<?php\nclass Base {}\n"));
session.ingest_file(dep_a.clone(), Arc::from("<?php\nclass A extends Base {}\n"));
session.ingest_file(dep_b.clone(), Arc::from("<?php\nclass B extends Base {}\n"));
for (file, src) in [
(&dep_a, "<?php\nclass A extends Base {}\n"),
(&dep_b, "<?php\nclass B extends Base {}\n"),
] {
let parsed = php_rs_parser::parse(src);
FileAnalyzer::new(&session).analyze(file.clone(), src, &parsed.program, &parsed.source_map);
}
assert!(session.source_of(dep_a.as_ref()).is_some());
assert_eq!(session.source_of("does-not-exist.php"), None);
let analyses = session.reanalyze_dependents(base.as_ref());
for (file, _) in &analyses {
assert!(file.as_ref() == dep_a.as_ref() || file.as_ref() == dep_b.as_ref());
}
}
#[test]
fn load_class_not_resolvable_without_resolver() {
use mir_analyzer::LoadOutcome;
let session = AnalysisSession::new(PhpVersion::LATEST);
let outcome = session.load_class("Some\\Unknown\\Class");
assert_eq!(outcome, LoadOutcome::NotResolvable);
}
#[test]
fn all_classes_and_all_functions_workspace_iteration() {
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ingest_file(
Arc::from("ws.php"),
Arc::from(
"<?php\n\
class Alpha {}\n\
class Beta {}\n\
function gamma(): void {}\n",
),
);
let classes = session.all_classes();
let class_names: Vec<&str> = classes.iter().map(|(f, _)| f.as_ref()).collect();
assert!(class_names.contains(&"Alpha"));
assert!(class_names.contains(&"Beta"));
let functions = session.all_functions();
let fn_names: Vec<&str> = functions.iter().map(|(f, _)| f.as_ref()).collect();
assert!(fn_names.contains(&"gamma"));
}
#[test]
fn reanalyze_dependents_tracks_bare_fqn_new() {
use mir_analyzer::FileAnalyzer;
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let service: Arc<str> = Arc::from("service.php");
let consumer: Arc<str> = Arc::from("consumer.php");
session.ingest_file(
service.clone(),
Arc::from("<?php\nclass Service { public function run(): void {} }\n"),
);
session.ingest_file(
consumer.clone(),
Arc::from("<?php\nfunction consume(): void { $s = new \\Service(); $s->run(); }\n"),
);
let consumer_src = "<?php\nfunction consume(): void { $s = new \\Service(); $s->run(); }\n";
let parsed = php_rs_parser::parse(consumer_src);
FileAnalyzer::new(&session).analyze(
consumer.clone(),
consumer_src,
&parsed.program,
&parsed.source_map,
);
let analyses = session.reanalyze_dependents(service.as_ref());
let dependent_files: Vec<&str> = analyses.iter().map(|(f, _)| f.as_ref()).collect();
assert!(
dependent_files.contains(&consumer.as_ref()),
"consumer.php references Service via bare FQN but was not returned by \
reanalyze_dependents — dependency graph is missing FQN reference edges"
);
}
#[test]
fn reanalyze_dependents_tracks_bare_fqn_static_call() {
use mir_analyzer::FileAnalyzer;
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let helper: Arc<str> = Arc::from("helper.php");
let caller: Arc<str> = Arc::from("caller.php");
session.ingest_file(
helper.clone(),
Arc::from("<?php\nclass Helper { public static function go(): void {} }\n"),
);
session.ingest_file(
caller.clone(),
Arc::from("<?php\nfunction call_it(): void { \\Helper::go(); }\n"),
);
let caller_src = "<?php\nfunction call_it(): void { \\Helper::go(); }\n";
let parsed = php_rs_parser::parse(caller_src);
FileAnalyzer::new(&session).analyze(
caller.clone(),
caller_src,
&parsed.program,
&parsed.source_map,
);
let analyses = session.reanalyze_dependents(helper.as_ref());
let dependent_files: Vec<&str> = analyses.iter().map(|(f, _)| f.as_ref()).collect();
assert!(
dependent_files.contains(&caller.as_ref()),
"caller.php references Helper via bare FQN static call but was not returned by \
reanalyze_dependents — dependency graph is missing FQN reference edges"
);
}
#[test]
fn dependency_graph_includes_unused_param_type_hint() {
use mir_analyzer::FileAnalyzer;
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let service: Arc<str> = Arc::from("service.php");
let consumer: Arc<str> = Arc::from("consumer.php");
session.ingest_file(
service.clone(),
Arc::from("<?php\nnamespace Vendor\nclass Service { }\n"),
);
session.ingest_file(
consumer.clone(),
Arc::from("<?php\nnamespace Vendor\nfunction consume(Service $s) { }\n"),
);
let consumer_src = "<?php\nnamespace Vendor\nfunction consume(Service $s) { }\n";
let parsed = php_rs_parser::parse(consumer_src);
FileAnalyzer::new(&session).analyze(
consumer.clone(),
consumer_src,
&parsed.program,
&parsed.source_map,
);
let dependents = session
.dependency_graph()
.transitive_dependents(service.as_ref());
assert!(
dependents.contains(&consumer.to_string()),
"consumer.php should depend on service.php due to type hint in parameter, \
even though the parameter is unused and there's no use statement"
);
}
fn analyze_file(session: &AnalysisSession, file: Arc<str>, src: &str) {
use mir_analyzer::FileAnalyzer;
let parsed = php_rs_parser::parse(src);
FileAnalyzer::new(session).analyze(file, src, &parsed.program, &parsed.source_map);
}
fn dependent_files(session: &AnalysisSession, file: &str) -> std::collections::HashSet<String> {
session
.reanalyze_dependents(file)
.into_iter()
.map(|(f, _)| f.to_string())
.collect()
}
#[test]
fn reanalyze_dependents_after_definition_deleted() {
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let foo: Arc<str> = Arc::from("Foo.php");
let bar: Arc<str> = Arc::from("Bar.php");
session.ingest_file(foo.clone(), Arc::from("<?php\nclass Foo {}\n"));
session.ingest_file(
bar.clone(),
Arc::from("<?php\nfunction f(\\Foo $x): void {}\n"),
);
let bar_src = "<?php\nfunction f(\\Foo $x): void {}\n";
analyze_file(&session, bar.clone(), bar_src);
let before = dependent_files(&session, foo.as_ref());
assert!(
before.contains(bar.as_ref()),
"precondition: Bar.php must be a dependent before deletion; got {:?}",
before
);
session.ingest_file(foo.clone(), Arc::from("<?php\n// class Foo removed\n"));
let after = dependent_files(&session, foo.as_ref());
assert!(
after.contains(bar.as_ref()),
"Bar.php references \\Foo which was deleted from Foo.php — \
it must still appear in reanalyze_dependents so the broken reference is surfaced; \
got {:?}",
after
);
}
#[test]
fn reanalyze_dependents_after_definition_renamed() {
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let foo: Arc<str> = Arc::from("Foo.php");
let bar: Arc<str> = Arc::from("Bar.php");
session.ingest_file(foo.clone(), Arc::from("<?php\nclass Foo {}\n"));
session.ingest_file(
bar.clone(),
Arc::from("<?php\nfunction f(\\Foo $x): void {}\n"),
);
analyze_file(
&session,
bar.clone(),
"<?php\nfunction f(\\Foo $x): void {}\n",
);
session.ingest_file(foo.clone(), Arc::from("<?php\nclass Renamed {}\n"));
let after = dependent_files(&session, foo.as_ref());
assert!(
after.contains(bar.as_ref()),
"Bar.php references \\Foo which was renamed to \\Renamed in Foo.php — \
Bar.php must still appear in reanalyze_dependents; got {:?}",
after
);
}
#[test]
fn reanalyze_dependents_after_definition_moved() {
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let a: Arc<str> = Arc::from("A.php");
let b: Arc<str> = Arc::from("B.php");
let consumer: Arc<str> = Arc::from("Consumer.php");
session.ingest_file(a.clone(), Arc::from("<?php\nclass Foo {}\n"));
session.ingest_file(b.clone(), Arc::from("<?php\n// empty\n"));
session.ingest_file(
consumer.clone(),
Arc::from("<?php\nfunction f(\\Foo $x): void {}\n"),
);
analyze_file(
&session,
consumer.clone(),
"<?php\nfunction f(\\Foo $x): void {}\n",
);
session.ingest_file(a.clone(), Arc::from("<?php\n// Foo moved to B.php\n"));
session.ingest_file(b.clone(), Arc::from("<?php\nclass Foo {}\n"));
let a_deps = dependent_files(&session, a.as_ref());
assert!(
a_deps.contains(consumer.as_ref()),
"Consumer.php must appear as dependent of A.php after Foo is moved out; got {:?}",
a_deps
);
let b_deps = dependent_files(&session, b.as_ref());
assert!(
b_deps.contains(consumer.as_ref()),
"Consumer.php must appear as dependent of B.php after Foo is moved in; got {:?}",
b_deps
);
}
#[test]
fn reanalyze_dependents_after_definition_readded() {
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let foo: Arc<str> = Arc::from("Foo.php");
let bar: Arc<str> = Arc::from("Bar.php");
session.ingest_file(foo.clone(), Arc::from("<?php\nclass Foo {}\n"));
session.ingest_file(
bar.clone(),
Arc::from("<?php\nfunction f(\\Foo $x): void {}\n"),
);
analyze_file(
&session,
bar.clone(),
"<?php\nfunction f(\\Foo $x): void {}\n",
);
session.ingest_file(foo.clone(), Arc::from("<?php\n// deleted\n"));
session.ingest_file(foo.clone(), Arc::from("<?php\nclass Foo {}\n"));
let after = dependent_files(&session, foo.as_ref());
assert!(
after.contains(bar.as_ref()),
"Bar.php must be a dependent of Foo.php after Foo is re-added; got {:?}",
after
);
}
#[test]
fn reanalyze_dependents_transitive_after_delete() {
let session = AnalysisSession::new(PhpVersion::LATEST);
session.ensure_all_stubs();
let a: Arc<str> = Arc::from("A.php");
let b: Arc<str> = Arc::from("B.php");
let c: Arc<str> = Arc::from("C.php");
session.ingest_file(a.clone(), Arc::from("<?php\nclass Foo {}\n"));
session.ingest_file(
b.clone(),
Arc::from("<?php\nclass Bar { public function f(\\Foo $x): void {} }\n"),
);
session.ingest_file(c.clone(), Arc::from("<?php\nclass Baz extends \\Bar {}\n"));
analyze_file(
&session,
b.clone(),
"<?php\nclass Bar { public function f(\\Foo $x): void {} }\n",
);
analyze_file(&session, c.clone(), "<?php\nclass Baz extends \\Bar {}\n");
session.ingest_file(a.clone(), Arc::from("<?php\n// Foo deleted\n"));
let after = dependent_files(&session, a.as_ref());
assert!(
after.contains(b.as_ref()),
"B.php (direct referencer of deleted Foo) must appear; got {:?}",
after
);
assert!(
after.contains(c.as_ref()),
"C.php (transitively depends on B.php) must appear; got {:?}",
after
);
}