1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
//! Regression: batch `analyze_paths` must resolve classes when the workspace
//! symbol index singleton was already built (from vendor) *before* the project
//! files were registered.
//!
//! The eager-indexing rework made `workspace_index` read a maintained singleton
//! and stopped nulling it when files are added. The CLI builds that singleton
//! from vendor (`index_vendor_chunked` / `collect_definitions`) BEFORE calling
//! `analyze_paths`, which then registers project files and lazy-loads referenced
//! classes — but nothing refreshed the singleton, so every project class and
//! every lazy-loaded class was absent from it and reported as a false
//! `UndefinedClass`. On the real Laravel tree this produced ~34k bogus
//! `UndefinedClass` (e.g. `Illuminate\Support\Str`, all real project classes).
//!
//! Precondition that triggers the bug: the singleton must be SET before
//! `analyze_paths` runs. A run that never pre-builds it falls back to the
//! always-correct tracked query and hides the bug — which is why small fixtures
//! and `run_plain_flow` never caught it.
mod common;
use std::fs;
use std::sync::Arc;
use mir_analyzer::{AnalysisSession, BatchOptions, IndexCancel, IndexParallelism, PhpVersion};
use mir_issues::IssueKind;
use self::common::create_temp_dir;
fn arc_pair(path: &std::path::Path) -> (Arc<str>, Arc<str>) {
let src = fs::read_to_string(path).unwrap();
(
Arc::from(path.to_string_lossy().as_ref()),
Arc::from(src.as_str()),
)
}
#[test]
fn batch_resolves_project_and_lazy_classes_with_prebuilt_index() {
let dir = create_temp_dir("batch_index_resolution");
let root = dir.path();
fs::create_dir_all(root.join("src")).unwrap();
fs::create_dir_all(root.join("vendor/acme/lib/src")).unwrap();
fs::create_dir_all(root.join("vendor/composer")).unwrap();
// Project: App\ -> src/. Vendor packages come from installed.json.
fs::write(
root.join("composer.json"),
r#"{ "name": "t/app", "autoload": { "psr-4": { "App\\": "src/" } } }"#,
)
.unwrap();
fs::write(
root.join("vendor/composer/installed.json"),
r#"{ "packages": [ { "name": "acme/lib", "autoload": { "psr-4": { "Acme\\": "src/" } } } ] }"#,
)
.unwrap();
// A vendor class used to PRE-BUILD the singleton (mirrors the CLI indexing
// vendor before analyze_paths).
fs::write(
root.join("vendor/acme/lib/src/Bootstrap.php"),
"<?php\nnamespace Acme;\nclass Bootstrap {}\n",
)
.unwrap();
// A vendor class that is NOT pre-indexed — it must be lazy-loaded AND merged
// into the singleton during analyze_paths.
fs::write(
root.join("vendor/acme/lib/src/Widget.php"),
"<?php\nnamespace Acme;\nclass Widget { public function name(): string { return \"w\"; } }\n",
)
.unwrap();
// A project class referenced by another project file (both are in the
// analyzed set, but absent from the vendor-only singleton until refreshed).
fs::write(
root.join("src/Helper.php"),
"<?php\nnamespace App;\nclass Helper { public function help(): string { return \"h\"; } }\n",
)
.unwrap();
let service = root.join("src/Service.php");
fs::write(
&service,
"<?php\nnamespace App;\nuse Acme\\Widget;\nclass Service {\n public function run(Widget $w, Helper $h): string { return $w->name() . $h->help(); }\n}\n",
)
.unwrap();
let psr4 = mir_analyzer::composer::Psr4Map::from_composer(root).expect("psr4");
let session = AnalysisSession::new(PhpVersion::LATEST).with_psr4(Arc::new(psr4));
session.ensure_all_stubs();
// PRE-BUILD the workspace index singleton from a vendor file, exactly as the
// CLI does before analyze_paths. This is the precondition that triggers the
// regression.
let cancel = IndexCancel::new();
let boot = arc_pair(&root.join("vendor/acme/lib/src/Bootstrap.php"));
session.index_batch(&[boot], IndexParallelism::Sequential, &cancel);
// Now analyze the project. `Acme\Widget` (lazy-loaded vendor) and `App\Helper`
// (sibling project class) are both real and must resolve.
let helper = root.join("src/Helper.php");
let result = session.analyze_paths(&[service.clone(), helper], &BatchOptions::new());
let undefined: Vec<&str> = result
.issues
.iter()
.filter_map(|i| match &i.kind {
IssueKind::UndefinedClass { name } => Some(name.as_str()),
_ => None,
})
.collect();
assert!(
undefined.is_empty(),
"real classes wrongly reported UndefinedClass after pre-built index: {:?}",
undefined
);
}