1use super::*;
2
3impl AnalysisSession {
4 pub fn analyze_paths(&self, paths: &[PathBuf], opts: &BatchOptions) -> AnalysisResult {
6 let php_version = self.batch_php_version(opts);
7 let mut all_issues = Vec::new();
8 let _t0 = std::time::Instant::now();
9
10 self.load_batch_stubs(php_version);
12 self.ensure_vendor_eager_functions();
16 let _t_stubs = _t0.elapsed();
17
18 let parsed_files: Vec<ParsedProjectFile> = paths
20 .par_iter()
21 .filter_map(|path| match std::fs::read_to_string(path) {
22 Ok(src) => {
23 let file = Arc::from(path.to_string_lossy().as_ref());
24 Some(ParsedProjectFile::new(file, Arc::from(src)))
25 }
26 Err(e) => {
27 eprintln!("Cannot read {}: {}", path.display(), e);
28 None
29 }
30 })
31 .collect();
32 let _t_read = _t0.elapsed();
33
34 let file_data: Vec<(Arc<str>, Arc<str>)> = parsed_files
35 .iter()
36 .map(|parsed| (parsed.file.clone(), parsed.source.clone()))
37 .collect();
38
39 let mut topology_changed = false;
52 let mut removed_files: Vec<String> = Vec::new();
53 if let Some(cache) = &self.cache {
54 let current: std::collections::HashSet<&str> =
55 file_data.iter().map(|(f, _)| f.as_ref()).collect();
56 removed_files = cache
57 .cached_files()
58 .into_iter()
59 .filter(|f| !current.contains(f.as_str()) && !std::path::Path::new(f).exists())
60 .collect();
61 for f in &removed_files {
62 cache.evict(f);
63 }
64 topology_changed = !removed_files.is_empty();
65 }
66
67 {
69 let mut guard = self.db.salsa.write();
70 for parsed in &parsed_files {
71 guard.upsert_source_file(parsed.file.clone(), parsed.source.clone());
72 }
73 }
74 let _t_salsa_reg = _t0.elapsed();
75
76 type Pass1Entry = (FileDefinitions, [u8; 32], bool, String);
80 let file_defs: Vec<Pass1Entry> = parsed_files
81 .par_iter()
82 .map(|parsed| {
83 let content_hash = hash_source(parsed.source());
84 let has_hard_parse_errors = parsed
85 .errors()
86 .iter()
87 .any(crate::parser::is_hard_parse_error);
88 let mut all_issues: Vec<Issue> = parsed
89 .errors()
90 .iter()
91 .filter(|err| !crate::parser::is_spurious_reserved_class_error(err))
92 .map(|err| {
93 crate::parser::parse_error_to_issue(
94 err,
95 &parsed.file,
96 parsed.source(),
97 parsed.source_map(),
98 )
99 })
100 .collect();
101 let collector = crate::collector::DefinitionCollector::new_for_slice(
102 parsed.file.clone(),
103 parsed.source(),
104 parsed.source_map(),
105 );
106 let (mut slice, collector_issues) = collector.collect_slice(parsed.owned());
107 all_issues.extend(collector_issues);
108 mir_codebase::storage::deduplicate_params_in_slice(&mut slice);
109 let defs = FileDefinitions {
110 slice: Arc::new(slice),
111 issues: Arc::new(all_issues),
112 };
113 let surface_hash = surface_fingerprint(parsed.source(), parsed.owned());
114 (defs, content_hash, has_hard_parse_errors, surface_hash)
115 })
116 .collect();
117 let _t_collect_defs = _t0.elapsed();
118
119 let surface_hashes: HashMap<Arc<str>, String> = parsed_files
122 .iter()
123 .zip(file_defs.iter())
124 .map(|(parsed, (_defs, _h, _e, surface))| (parsed.file.clone(), surface.clone()))
125 .collect();
126
127 if let Some(cache) = &self.cache {
134 let content_changed: Vec<String> = file_data
135 .par_iter()
136 .filter_map(|(f, src)| {
137 let h = hash_content(src.as_ref());
138 if cache.get(f, &h).is_none() {
139 Some(f.to_string())
140 } else {
141 None
142 }
143 })
144 .collect();
145 if !content_changed.is_empty() {
146 topology_changed = true;
147 }
148 let mut seeds: Vec<String> = content_changed
149 .into_iter()
150 .filter(|f| {
151 let new_surface = surface_hashes
152 .get(f.as_str())
153 .map(String::as_str)
154 .unwrap_or("");
155 !matches!(
157 cache.surface_hash(f),
158 Some(stored) if !stored.is_empty() && stored == new_surface
159 )
160 })
161 .collect();
162 seeds.extend(removed_files.iter().cloned());
163 if !seeds.is_empty() {
164 cache.evict_with_dependents(&seeds);
165 }
166 }
167
168 {
171 let guard = self.db.salsa.read();
172 for (defs, hash, has_hard_parse_errors, _surface) in &file_defs {
173 if !*has_hard_parse_errors {
174 guard.prime_parse_cache(*hash, Arc::clone(&defs.slice));
175 }
176 }
177 }
178
179 let mut files_with_parse_errors: HashSet<Arc<str>> = HashSet::default();
180 for (defs, _hash, _hard_err, _surface) in file_defs {
181 for issue in defs.issues.iter() {
182 if matches!(issue.kind, mir_issues::IssueKind::ParseError { .. })
183 && issue.severity == mir_issues::Severity::Error
184 {
185 files_with_parse_errors.insert(issue.location.file.clone());
186 }
187 }
188 all_issues.extend(Arc::unwrap_or_clone(defs.issues));
189 }
190 let _t_ingest = _t0.elapsed();
191
192 {
194 let db_prewarm = {
195 let guard = self.db.salsa.read();
196 (**guard).clone()
197 };
198 let project_source_files: Vec<SourceFile> = {
199 let guard = self.db.salsa.read();
200 parsed_files
201 .iter()
202 .filter_map(|p| (**guard).lookup_source_file(&p.file))
203 .collect()
204 };
205 project_source_files
206 .into_par_iter()
207 .for_each_with(db_prewarm, |db, sf| {
208 let _ = collect_file_definitions(db as &dyn MirDatabase, sf);
209 });
210 }
211 let _t_prewarm_ms = (_t0.elapsed() - _t_ingest).as_secs_f64() * 1000.0;
212
213 self.refresh_workspace_index();
219
220 let _t_before_lazy = _t0.elapsed();
222 if let Some(psr4) = self.psr4.clone() {
223 self.lazy_load_missing_classes(psr4, php_version, &mut all_issues);
224 }
225 let _t_lazyload_ms = (_t0.elapsed() - _t_before_lazy).as_secs_f64() * 1000.0;
226
227 let analyzed_file_set: HashSet<Arc<str>> =
229 file_data.iter().map(|(f, _)| f.clone()).collect();
230 let _t_class_analyzer = std::time::Instant::now();
231 {
232 let class_db = {
233 let guard = self.db.salsa.read();
234 (**guard).clone()
235 };
236 let class_issues = crate::class::ClassAnalyzer::with_files(
237 &class_db,
238 analyzed_file_set.clone(),
239 &file_data,
240 )
241 .analyze_all();
242 all_issues.extend(class_issues);
243 }
244 let _t_class_analyzer_ms = _t_class_analyzer.elapsed().as_secs_f64() * 1000.0;
245
246 let _t_class_checks = _t0.elapsed();
247
248 let mut db_main = {
249 let guard = self.db.salsa.read();
250 (**guard).clone()
251 };
252 db_main.freeze_workspace_index();
258
259 type BodyResult = (
261 Arc<str>,
262 Vec<Issue>,
263 Vec<crate::symbol::ResolvedSymbol>,
264 Vec<RefLoc>,
265 );
266 let body_results: Vec<BodyResult> = parsed_files
267 .par_iter()
268 .filter(|parsed| !files_with_parse_errors.contains(&parsed.file))
269 .map_with(db_main, |db, parsed| {
270 let driver = BodyAnalyzer::new(&*db as &dyn MirDatabase, php_version);
271 let (issues, symbols) = if let Some(cache) = &self.cache {
272 let h = hash_content(parsed.source());
273 if let Some((cached_issues, ref_locs)) = cache.get(&parsed.file, &h) {
274 let locs: Vec<RefLoc> = ref_locs
278 .iter()
279 .map(|(symbol, line, col_start, col_end)| RefLoc {
280 symbol_key: Arc::from(symbol.as_str()),
281 file: parsed.file.clone(),
282 line: *line,
283 col_start: *col_start,
284 col_end: *col_end,
285 })
286 .collect();
287 return (parsed.file.clone(), cached_issues, Vec::new(), locs);
288 }
289 let (issues, symbols) = driver.analyze_bodies(
290 parsed.owned(),
291 parsed.file.clone(),
292 parsed.source(),
293 parsed.source_map(),
294 );
295 let pending = db.take_pending_ref_locs();
296 let cache_locs = pending
297 .iter()
298 .map(|r| (r.symbol_key.to_string(), r.line, r.col_start, r.col_end))
299 .collect();
300 let surface = surface_hashes
301 .get(parsed.file.as_ref())
302 .cloned()
303 .unwrap_or_default();
304 cache.put(&parsed.file, h, surface, issues.clone(), cache_locs);
305 if let Some(cb) = &opts.on_file_done {
306 cb();
307 }
308 let symbols = if opts.skip_symbols {
309 Vec::new()
310 } else {
311 symbols
312 };
313 return (parsed.file.clone(), issues, symbols, pending);
314 } else {
315 driver.analyze_bodies(
316 parsed.owned(),
317 parsed.file.clone(),
318 parsed.source(),
319 parsed.source_map(),
320 )
321 };
322 let pending = db.take_pending_ref_locs();
323 if let Some(cb) = &opts.on_file_done {
324 cb();
325 }
326 let symbols = if opts.skip_symbols {
329 Vec::new()
330 } else {
331 symbols
332 };
333 (parsed.file.clone(), issues, symbols, pending)
334 })
335 .collect();
336
337 let _t_body_analysis = _t0.elapsed();
338
339 let mut all_symbols = Vec::new();
343 {
344 let guard = self.db.salsa.read();
345 for (file, issues, symbols, ref_locs) in body_results {
346 all_issues.extend(issues);
347 all_symbols.extend(symbols);
348 guard.set_file_reference_locations(file.as_ref(), ref_locs);
349 }
350 }
351
352 if let Some(psr4) = self.psr4.clone() {
354 self.lazy_load_from_body_issues(
355 psr4,
356 php_version,
357 &file_data,
358 &files_with_parse_errors,
359 &mut all_issues,
360 &mut all_symbols,
361 opts.skip_symbols,
362 );
363 }
364
365 if topology_changed {
373 if let Some(cache) = &self.cache {
374 let db_snapshot = {
375 let guard = self.db.salsa.read();
376 (**guard).clone()
377 };
378 let rev = build_reverse_deps(&db_snapshot);
379 cache.set_reverse_deps(rev);
380 }
381 }
382
383 if let Some(cache) = &self.cache {
385 cache.flush();
386 }
387
388 if opts.should_run_dead_code() {
390 let salsa = self.snapshot_db();
391 let _t_dead_code = std::time::Instant::now();
392 let dead_code_issues =
393 crate::dead_code::DeadCodeAnalyzer::with_files(&salsa, analyzed_file_set.clone())
394 .analyze();
395 all_issues.extend(dead_code_issues);
396 if std::env::var("MIR_TIMING").is_ok() {
397 eprintln!(
398 "[timing] dead_code_analyzer={:.0}ms",
399 _t_dead_code.elapsed().as_secs_f64() * 1000.0
400 );
401 }
402 }
403
404 let _t_total = _t0.elapsed();
405 if std::env::var("MIR_TIMING").is_ok() {
406 eprintln!(
407 "[timing] stubs={:.0}ms read={:.0}ms salsa_reg={:.0}ms collect_defs={:.0}ms ingest={:.0}ms class_checks={:.0}ms (prewarm={:.0}ms lazy_load={:.0}ms class_analyzer={:.0}ms) body_analysis={:.0}ms total={:.0}ms",
408 _t_stubs.as_secs_f64() * 1000.0,
409 (_t_read - _t_stubs).as_secs_f64() * 1000.0,
410 (_t_salsa_reg - _t_read).as_secs_f64() * 1000.0,
411 (_t_collect_defs - _t_salsa_reg).as_secs_f64() * 1000.0,
412 (_t_ingest - _t_collect_defs).as_secs_f64() * 1000.0,
413 (_t_class_checks - _t_ingest).as_secs_f64() * 1000.0,
414 _t_prewarm_ms,
415 _t_lazyload_ms,
416 _t_class_analyzer_ms,
417 (_t_body_analysis - _t_class_checks).as_secs_f64() * 1000.0,
418 _t_total.as_secs_f64() * 1000.0,
419 );
420 }
421
422 opts.apply(&mut all_issues);
423 let analyzed_files_vec: Vec<Arc<str>> = analyzed_file_set.iter().cloned().collect();
424 self.apply_suppressions_and_emit_unused(&mut all_issues, &analyzed_files_vec);
425 if let Some(dump) = crate::metrics::dump() {
426 eprintln!("{dump}");
427 }
428
429 {
431 let mut guard = self.db.salsa.write();
432 guard.rebuild_workspace_symbol_index();
433 }
434
435 AnalysisResult::build(all_issues, rustc_hash::FxHashMap::default(), all_symbols)
436 }
437 pub fn re_analyze_file(
443 &self,
444 file_path: &str,
445 new_content: &str,
446 opts: &BatchOptions,
447 ) -> AnalysisResult {
448 let php_version = self.batch_php_version(opts);
449
450 if let Some(cache) = &self.cache {
452 let h = hash_content(new_content);
453 if let Some((mut issues, ref_locs)) = cache.get(file_path, &h) {
454 let file: Arc<str> = Arc::from(file_path);
455 let locs: Vec<RefLoc> = ref_locs
459 .iter()
460 .map(|(symbol, line, col_start, col_end)| RefLoc {
461 symbol_key: Arc::from(symbol.as_str()),
462 file: file.clone(),
463 line: *line,
464 col_start: *col_start,
465 col_end: *col_end,
466 })
467 .collect();
468 let guard = self.db.salsa.read();
469 guard.set_file_reference_locations(file_path, locs);
470 drop(guard);
471 opts.apply(&mut issues);
472 self.apply_suppressions_and_emit_unused(&mut issues, std::slice::from_ref(&file));
473 return AnalysisResult::build(issues, HashMap::default(), Vec::new());
474 }
475 }
476
477 let file: Arc<str> = Arc::from(file_path);
478
479 {
480 let mut guard = self.db.salsa.write();
481 guard.remove_file_definitions(file_path);
482 }
483
484 let file_defs = {
485 let mut guard = self.db.salsa.write();
486 let salsa_file = guard.upsert_source_file(file.clone(), Arc::from(new_content));
487 collect_file_definitions(&**guard, salsa_file)
488 };
489
490 let mut all_issues: Vec<Issue> = Arc::unwrap_or_clone(file_defs.issues.clone());
491
492 {
493 let mut guard = self.db.salsa.write();
494 if guard.workspace_symbol_index_singleton().is_some() {
495 if let Some(sf) = guard.lookup_source_file(file.as_ref()) {
496 if guard.file_declarations_changed(sf) {
497 guard.rebuild_workspace_symbol_index();
498 }
499 }
500 }
501 }
502
503 let (symbols, surface_hash) = {
504 let guard = self.db.salsa.write();
505
506 let parsed = php_rs_parser::parse(new_content);
507 let surface_hash = surface_fingerprint(new_content, &parsed.program);
508
509 let has_hard_errors = parsed.errors.iter().any(crate::parser::is_hard_parse_error);
510 let symbols = if !has_hard_errors {
511 let db_ref: &dyn MirDatabase = &**guard;
512 let driver = BodyAnalyzer::new(db_ref, php_version);
513 let (body_issues, symbols) = driver.analyze_bodies(
514 &parsed.program,
515 file.clone(),
516 new_content,
517 &parsed.source_map,
518 );
519 all_issues.extend(body_issues);
520 let pending = guard.take_pending_ref_locs();
521 guard.set_file_reference_locations(file.as_ref(), pending);
522 symbols
523 } else {
524 Vec::new()
525 };
526 (symbols, surface_hash)
527 };
528
529 mark_suppressed(
537 &mut all_issues,
538 &crate::suppression::SuppressionMap::from_source(new_content),
539 );
540
541 if let Some(cache) = &self.cache {
542 let h = hash_content(new_content);
543 let surface_changed = match cache.surface_hash(file_path) {
547 Some(stored) if !stored.is_empty() => stored != surface_hash,
548 _ => true,
549 };
550 if surface_changed {
551 cache.evict_with_dependents(&[file_path.to_string()]);
552 }
553 let db = self.snapshot_db();
554 let ref_locs = extract_reference_locations(&db, &file);
555 cache.put(file_path, h, surface_hash, all_issues.clone(), ref_locs);
556 }
557
558 opts.apply(&mut all_issues);
559 AnalysisResult::build(all_issues, HashMap::default(), symbols)
560 }
561
562 pub fn collect_definitions(&self, paths: &[PathBuf]) {
571 let _timing = std::env::var("MIR_TIMING").is_ok();
572 let _t0 = std::time::Instant::now();
573
574 let php_v = self.php_version.cache_byte();
575
576 struct FileEntry {
577 file: Arc<str>,
578 src: Arc<str>,
579 hash: [u8; 32],
580 cached: Option<mir_codebase::storage::StubSlice>,
581 }
582 let entries: Vec<FileEntry> = paths
583 .par_iter()
584 .filter_map(|path| {
585 let src = std::fs::read_to_string(path).ok()?;
586 let file: Arc<str> = Arc::from(path.to_string_lossy().as_ref());
587 let src: Arc<str> = Arc::from(src);
588 let hash = hash_source(&src);
589 let cached = self.db.stub_cache.as_ref().and_then(|c| {
590 let mut slice = c.get(&file, &hash, php_v)?;
591 prepare_for_ingest(&mut slice);
592 Some(slice)
593 });
594 Some(FileEntry {
595 file,
596 src,
597 hash,
598 cached,
599 })
600 })
601 .collect();
602 let _t_read = _t0.elapsed();
603
604 let source_files: Vec<SourceFile> = {
605 let mut guard = self.db.salsa.write();
606 entries
607 .iter()
608 .map(|e| {
609 guard.upsert_source_file_with_durability(
610 e.file.clone(),
611 e.src.clone(),
612 salsa::Durability::HIGH,
613 )
614 })
615 .collect()
616 };
617 let _t_reg = _t0.elapsed();
618
619 let db_pass1 = {
620 let guard = self.db.salsa.read();
621 (**guard).clone()
622 };
623 let stub_cache = self.db.stub_cache.clone();
624 let prepared: Vec<mir_codebase::storage::StubSlice> = entries
625 .into_par_iter()
626 .zip(source_files.into_par_iter())
627 .map_with(db_pass1, |db, (mut entry, salsa_file)| {
628 if let Some(slice) = entry.cached.take() {
629 let slice_arc = Arc::new(slice);
630 db.parse_cache().insert(entry.hash, Arc::clone(&slice_arc));
631 return (*slice_arc).clone();
632 }
633 let defs = collect_file_definitions(&*db, salsa_file);
634 if let Some(cache) = stub_cache.as_ref() {
635 cache.put(&entry.file, &entry.hash, php_v, &defs.slice);
636 }
637 (*defs.slice).clone()
638 })
639 .collect();
640 let _t_collect = _t0.elapsed();
641 drop(prepared);
642 let _t_ingest = _t0.elapsed();
643
644 if _timing {
645 let (hits, misses) = self.stub_cache_stats();
646 eprintln!(
647 "[vendor] read={:.0}ms reg={:.0}ms collect={:.0}ms ingest={:.0}ms total={:.0}ms (cache hits={hits} misses={misses})",
648 _t_read.as_secs_f64() * 1000.0,
649 (_t_reg - _t_read).as_secs_f64() * 1000.0,
650 (_t_collect - _t_reg).as_secs_f64() * 1000.0,
651 (_t_ingest - _t_collect).as_secs_f64() * 1000.0,
652 _t_ingest.as_secs_f64() * 1000.0,
653 );
654 }
655
656 {
657 let mut guard = self.db.salsa.write();
658 guard.rebuild_workspace_symbol_index();
659 }
660
661 crate::collector::print_collector_stats();
662 }
663}