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: rustc_hash::FxHashSet<&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::definitions::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 let content_hexes: HashMap<Arc<str>, String> = parsed_files
131 .iter()
132 .zip(file_defs.iter())
133 .map(|(parsed, (_defs, hash, _e, _surface))| {
134 (
135 parsed.file.clone(),
136 blake3::Hash::from(*hash).to_hex().to_string(),
137 )
138 })
139 .collect();
140
141 if let Some(cache) = &self.cache {
148 let content_changed: Vec<String> = file_data
149 .iter()
150 .filter_map(|(f, _src)| {
151 let valid = content_hexes.get(f).is_some_and(|h| cache.is_valid(f, h));
152 if valid {
153 None
154 } else {
155 Some(f.to_string())
156 }
157 })
158 .collect();
159 if !content_changed.is_empty() {
160 topology_changed = true;
161 }
162 let mut seeds: Vec<String> = content_changed
163 .into_iter()
164 .filter(|f| {
165 let new_surface = surface_hashes
166 .get(f.as_str())
167 .map(String::as_str)
168 .unwrap_or("");
169 !matches!(
171 cache.surface_hash(f),
172 Some(stored) if !stored.is_empty() && stored == new_surface
173 )
174 })
175 .collect();
176 seeds.extend(removed_files.iter().cloned());
177 if !seeds.is_empty() {
178 cache.evict_with_dependents(&seeds);
179 }
180 }
181
182 {
185 let guard = self.db.salsa.read();
186 let php_v = php_version.cache_byte();
187 for (defs, hash, has_hard_parse_errors, _surface) in &file_defs {
188 if !*has_hard_parse_errors {
189 guard.prime_parse_cache(
190 *hash,
191 php_v,
192 Arc::clone(&defs.slice),
193 Arc::clone(&defs.issues),
194 );
195 }
196 }
197 }
198
199 let mut files_with_parse_errors: HashSet<Arc<str>> = HashSet::default();
200 {
201 let guard = self.db.salsa.read();
206 for (parsed, (defs, _hash, _hard_err, _surface)) in parsed_files.iter().zip(file_defs) {
207 for issue in defs.issues.iter() {
208 if matches!(issue.kind, mir_issues::IssueKind::ParseError { .. })
209 && issue.severity == mir_issues::Severity::Error
210 {
211 files_with_parse_errors.insert(issue.location.file.clone());
212 }
213 }
214 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
215 guard.set_file_class_edges(&parsed.file, entries);
216 all_issues.extend(Arc::unwrap_or_clone(defs.issues));
217 }
218 }
219 let _t_ingest = _t0.elapsed();
220
221 {
223 let db_prewarm = {
224 let guard = self.db.salsa.read();
225 (**guard).clone()
226 };
227 let project_source_files: Vec<SourceFile> = {
228 let guard = self.db.salsa.read();
229 parsed_files
230 .iter()
231 .filter_map(|p| (**guard).lookup_source_file(&p.file))
232 .collect()
233 };
234 project_source_files
235 .into_par_iter()
236 .for_each_with(db_prewarm, |db, sf| {
237 let _ = collect_file_definitions(db as &dyn MirDatabase, sf);
238 });
239 }
240 let _t_prewarm_ms = (_t0.elapsed() - _t_ingest).as_secs_f64() * 1000.0;
241
242 self.refresh_workspace_index();
248
249 let _t_before_lazy = _t0.elapsed();
251 if let Some(psr4) = self.psr4.clone() {
252 self.lazy_load_missing_classes(psr4, php_version, &mut all_issues);
253 }
254 let _t_lazyload_ms = (_t0.elapsed() - _t_before_lazy).as_secs_f64() * 1000.0;
255
256 let analyzed_file_set: HashSet<Arc<str>> =
258 file_data.iter().map(|(f, _)| f.clone()).collect();
259
260 if let Some(plugins) = mir_plugin::snapshot() {
263 if plugins.hooks().after_codebase_populated {
264 let files: Vec<Arc<str>> = analyzed_file_set.iter().cloned().collect();
265 plugins.after_codebase_populated(&mut mir_plugin::AfterCodebasePopulatedEvent {
266 files: &files,
267 });
268 }
269 }
270 let _t_class_analyzer = std::time::Instant::now();
271 {
272 let class_db = {
273 let guard = self.db.salsa.read();
274 (**guard).clone()
275 };
276 let class_issues = crate::class::ClassAnalyzer::with_files(
277 &class_db,
278 analyzed_file_set.clone(),
279 &file_data,
280 )
281 .analyze_all();
282 all_issues.extend(class_issues);
283 }
284 let _t_class_analyzer_ms = _t_class_analyzer.elapsed().as_secs_f64() * 1000.0;
285
286 let _t_class_checks = _t0.elapsed();
287
288 let mut db_main = {
289 let guard = self.db.salsa.read();
290 (**guard).clone()
291 };
292 db_main.freeze_workspace_index();
298
299 type BodyResult = (
301 Arc<str>,
302 Vec<Issue>,
303 Vec<crate::symbol::ResolvedSymbol>,
304 Vec<RefLoc>,
305 );
306 let body_results: Vec<BodyResult> = parsed_files
307 .par_iter()
308 .filter(|parsed| !files_with_parse_errors.contains(&parsed.file))
309 .map_with(db_main, |db, parsed| {
310 let mut driver = BodyAnalyzer::new(&*db as &dyn MirDatabase, php_version);
311 driver.collect_symbols = !opts.skip_symbols;
314 let (issues, symbols) = if let Some(cache) = &self.cache {
315 let h = content_hexes
316 .get(parsed.file.as_ref())
317 .cloned()
318 .unwrap_or_else(|| hash_content(parsed.source()));
319 if let Some((cached_issues, ref_locs)) = cache.get(&parsed.file, &h) {
320 let locs: Vec<RefLoc> = ref_locs
325 .iter()
326 .map(|(symbol, line, col_start, col_end)| RefLoc {
327 symbol_key: Arc::clone(symbol),
328 file: parsed.file.clone(),
329 line: *line,
330 col_start: *col_start,
331 col_end: *col_end,
332 })
333 .collect();
334 return (
335 parsed.file.clone(),
336 cached_issues.to_vec(),
337 Vec::new(),
338 locs,
339 );
340 }
341 let (issues, symbols) = driver.analyze_bodies(
342 parsed.owned(),
343 parsed.file.clone(),
344 parsed.source(),
345 parsed.source_map(),
346 );
347 let pending = db.take_pending_ref_locs();
348 let cache_locs: Arc<[crate::cache::CachedRefLoc]> = pending
349 .iter()
350 .map(|r| (Arc::clone(&r.symbol_key), r.line, r.col_start, r.col_end))
351 .collect();
352 let surface = surface_hashes
353 .get(parsed.file.as_ref())
354 .cloned()
355 .unwrap_or_default();
356 cache.put(
357 &parsed.file,
358 h,
359 surface,
360 issues.as_slice().into(),
361 cache_locs,
362 );
363 if let Some(cb) = &opts.on_file_done {
364 cb();
365 }
366 let symbols = if opts.skip_symbols {
367 Vec::new()
368 } else {
369 symbols
370 };
371 return (parsed.file.clone(), issues, symbols, pending);
372 } else {
373 driver.analyze_bodies(
374 parsed.owned(),
375 parsed.file.clone(),
376 parsed.source(),
377 parsed.source_map(),
378 )
379 };
380 let pending = db.take_pending_ref_locs();
381 if let Some(cb) = &opts.on_file_done {
382 cb();
383 }
384 let symbols = if opts.skip_symbols {
387 Vec::new()
388 } else {
389 symbols
390 };
391 (parsed.file.clone(), issues, symbols, pending)
392 })
393 .collect();
394
395 let _t_body_analysis = _t0.elapsed();
396
397 let mut all_symbols = Vec::new();
401 {
402 let guard = self.db.salsa.read();
403 for (file, issues, symbols, ref_locs) in body_results {
404 all_issues.extend(issues);
405 all_symbols.extend(symbols);
406 guard.set_file_reference_locations(file.as_ref(), ref_locs);
407 }
408 }
409
410 if let Some(psr4) = self.psr4.clone() {
412 self.lazy_load_from_body_issues(
413 psr4,
414 php_version,
415 &file_data,
416 &files_with_parse_errors,
417 &mut all_issues,
418 &mut all_symbols,
419 opts.skip_symbols,
420 );
421 }
422
423 if topology_changed {
431 if let Some(cache) = &self.cache {
432 let db_snapshot = {
433 let guard = self.db.salsa.read();
434 (**guard).clone()
435 };
436 let rev = build_reverse_deps(&db_snapshot);
437 cache.set_reverse_deps(rev);
438 }
439 }
440
441 if let Some(cache) = &self.cache {
443 cache.flush();
444 }
445
446 if opts.should_run_dead_code() {
448 let salsa = self.snapshot_db();
449 let _t_dead_code = std::time::Instant::now();
450 let dead_code_issues =
451 crate::dead_code::DeadCodeAnalyzer::with_files(&salsa, analyzed_file_set.clone())
452 .analyze();
453 all_issues.extend(dead_code_issues);
454 if std::env::var("MIR_TIMING").is_ok() {
455 eprintln!(
456 "[timing] dead_code_analyzer={:.0}ms",
457 _t_dead_code.elapsed().as_secs_f64() * 1000.0
458 );
459 }
460 }
461
462 let _t_total = _t0.elapsed();
463 if std::env::var("MIR_TIMING").is_ok() {
464 eprintln!(
465 "[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",
466 _t_stubs.as_secs_f64() * 1000.0,
467 (_t_read - _t_stubs).as_secs_f64() * 1000.0,
468 (_t_salsa_reg - _t_read).as_secs_f64() * 1000.0,
469 (_t_collect_defs - _t_salsa_reg).as_secs_f64() * 1000.0,
470 (_t_ingest - _t_collect_defs).as_secs_f64() * 1000.0,
471 (_t_class_checks - _t_ingest).as_secs_f64() * 1000.0,
472 _t_prewarm_ms,
473 _t_lazyload_ms,
474 _t_class_analyzer_ms,
475 (_t_body_analysis - _t_class_checks).as_secs_f64() * 1000.0,
476 _t_total.as_secs_f64() * 1000.0,
477 );
478 }
479
480 if let Some(plugins) = mir_plugin::snapshot() {
483 if plugins.hooks().before_add_issue {
484 all_issues.retain(|i| plugins.before_add_issue(i));
485 }
486 }
487
488 opts.apply(&mut all_issues);
489 let analyzed_files_vec: Vec<Arc<str>> = analyzed_file_set.iter().cloned().collect();
490 self.apply_suppressions_and_emit_unused(&mut all_issues, &analyzed_files_vec);
491 if let Some(dump) = crate::metrics::dump() {
492 eprintln!("{dump}");
493 }
494
495 {
497 let mut guard = self.db.salsa.write();
498 guard.rebuild_workspace_symbol_index();
499 }
500
501 AnalysisResult::build(all_issues, rustc_hash::FxHashMap::default(), all_symbols)
502 }
503 pub fn re_analyze_file(
509 &self,
510 file_path: &str,
511 new_content: &str,
512 opts: &BatchOptions,
513 ) -> AnalysisResult {
514 let php_version = self.batch_php_version(opts);
515
516 if let Some(cache) = &self.cache {
518 let h = hash_content(new_content);
519 if let Some((cached_issues, ref_locs)) = cache.get(file_path, &h) {
520 let mut issues = cached_issues.to_vec();
521 let file: Arc<str> = Arc::from(file_path);
522 let locs: Vec<RefLoc> = ref_locs
526 .iter()
527 .map(|(symbol, line, col_start, col_end)| RefLoc {
528 symbol_key: Arc::clone(symbol),
529 file: file.clone(),
530 line: *line,
531 col_start: *col_start,
532 col_end: *col_end,
533 })
534 .collect();
535 let guard = self.db.salsa.read();
536 guard.set_file_reference_locations(file_path, locs);
537 drop(guard);
538 opts.apply(&mut issues);
539 self.apply_suppressions_and_emit_unused(&mut issues, std::slice::from_ref(&file));
540 return AnalysisResult::build(issues, HashMap::default(), Vec::new());
541 }
542 }
543
544 let file: Arc<str> = Arc::from(file_path);
545
546 {
547 let mut guard = self.db.salsa.write();
548 guard.remove_file_definitions(file_path);
549 }
550
551 let file_defs = {
552 let mut guard = self.db.salsa.write();
553 let salsa_file = guard.upsert_source_file(file.clone(), Arc::from(new_content));
554 collect_file_definitions(&**guard, salsa_file).clone()
555 };
556
557 let mut all_issues: Vec<Issue> = Arc::unwrap_or_clone(file_defs.issues.clone());
558
559 {
560 let mut guard = self.db.salsa.write();
561 if guard.workspace_symbol_index_singleton().is_some() {
562 if let Some(sf) = guard.lookup_source_file(file.as_ref()) {
563 if guard.file_declarations_changed(sf) {
564 guard.rebuild_workspace_symbol_index();
565 }
566 }
567 }
568 }
569
570 let (symbols, surface_hash) = {
571 let guard = self.db.salsa.write();
572
573 let parsed = php_rs_parser::parse(new_content);
574 let surface_hash = surface_fingerprint(new_content, &parsed.program);
575
576 let has_hard_errors = parsed.errors.iter().any(crate::parser::is_hard_parse_error);
577 let symbols = if !has_hard_errors {
578 let db_ref: &dyn MirDatabase = &**guard;
579 let driver = BodyAnalyzer::new(db_ref, php_version);
580 let (body_issues, symbols) = driver.analyze_bodies(
581 &parsed.program,
582 file.clone(),
583 new_content,
584 &parsed.source_map,
585 );
586 all_issues.extend(body_issues);
587 let pending = guard.take_pending_ref_locs();
588 guard.set_file_reference_locations(file.as_ref(), pending);
589 symbols
590 } else {
591 Vec::new()
592 };
593 (symbols, surface_hash)
594 };
595
596 mark_suppressed(
604 &mut all_issues,
605 &crate::suppression::SuppressionMap::from_source(new_content),
606 );
607
608 if let Some(cache) = &self.cache {
609 let h = hash_content(new_content);
610 let surface_changed = match cache.surface_hash(file_path) {
614 Some(stored) if !stored.is_empty() => stored != surface_hash,
615 _ => true,
616 };
617 if surface_changed {
618 cache.evict_with_dependents(&[file_path.to_string()]);
619 }
620 let db = self.snapshot_db();
621 let ref_locs = extract_reference_locations(&db, &file);
622 cache.put(
623 file_path,
624 h,
625 surface_hash,
626 all_issues.as_slice().into(),
627 ref_locs,
628 );
629 }
630
631 opts.apply(&mut all_issues);
632 self.apply_suppressions_and_emit_unused(&mut all_issues, std::slice::from_ref(&file));
640 AnalysisResult::build(all_issues, HashMap::default(), symbols)
641 }
642
643 pub fn collect_definitions(&self, paths: &[PathBuf]) {
652 let _timing = std::env::var("MIR_TIMING").is_ok();
653 let _t0 = std::time::Instant::now();
654
655 let php_v = self.php_version.cache_byte();
656
657 struct FileEntry {
658 file: Arc<str>,
659 src: Arc<str>,
660 hash: [u8; 32],
661 cached: Option<mir_codebase::definitions::StubSlice>,
662 }
663 let entries: Vec<FileEntry> = paths
664 .par_iter()
665 .filter_map(|path| {
666 let src = std::fs::read_to_string(path).ok()?;
667 let file: Arc<str> = Arc::from(path.to_string_lossy().as_ref());
668 let src: Arc<str> = Arc::from(src);
669 let hash = hash_source(&src);
670 let cached = self.db.stub_cache.as_ref().and_then(|c| {
671 let (mut slice, _issues) = c.get(&file, &hash, php_v)?;
672 prepare_for_ingest(&mut slice);
673 Some(slice)
674 });
675 Some(FileEntry {
676 file,
677 src,
678 hash,
679 cached,
680 })
681 })
682 .collect();
683 let _t_read = _t0.elapsed();
684
685 let source_files: Vec<SourceFile> = {
686 let mut guard = self.db.salsa.write();
687 entries
688 .iter()
689 .map(|e| {
690 guard.upsert_source_file_with_durability(
691 e.file.clone(),
692 e.src.clone(),
693 salsa::Durability::HIGH,
694 )
695 })
696 .collect()
697 };
698 let _t_reg = _t0.elapsed();
699
700 let db_pass1 = {
701 let guard = self.db.salsa.read();
702 (**guard).clone()
703 };
704 let stub_cache = self.db.stub_cache.clone();
705 let prepared: Vec<(Arc<str>, mir_codebase::definitions::StubSlice)> = entries
706 .into_par_iter()
707 .zip(source_files.into_par_iter())
708 .map_with(db_pass1, |db, (mut entry, salsa_file)| {
709 if let Some(slice) = entry.cached.take() {
710 let slice_arc = Arc::new(slice);
711 db.parse_cache().insert(
712 entry.hash,
713 php_v,
714 Arc::clone(&slice_arc),
715 Arc::new(Vec::new()),
716 );
717 return (entry.file.clone(), (*slice_arc).clone());
718 }
719 let defs = collect_file_definitions(&*db, salsa_file);
720 if let Some(cache) = stub_cache.as_ref() {
721 cache.put(&entry.file, &entry.hash, php_v, &defs.slice, &defs.issues);
722 }
723 (entry.file.clone(), (*defs.slice).clone())
724 })
725 .collect();
726 let _t_collect = _t0.elapsed();
727 {
733 let guard = self.db.salsa.read();
734 for (file, slice) in &prepared {
735 let entries = crate::db::subtype_index::entries_from_slice(slice);
736 guard.set_file_class_edges(file, entries);
737 }
738 }
739 drop(prepared);
740 let _t_ingest = _t0.elapsed();
741
742 if _timing {
743 let (hits, misses) = self.stub_cache_stats();
744 eprintln!(
745 "[vendor] read={:.0}ms reg={:.0}ms collect={:.0}ms ingest={:.0}ms total={:.0}ms (cache hits={hits} misses={misses})",
746 _t_read.as_secs_f64() * 1000.0,
747 (_t_reg - _t_read).as_secs_f64() * 1000.0,
748 (_t_collect - _t_reg).as_secs_f64() * 1000.0,
749 (_t_ingest - _t_collect).as_secs_f64() * 1000.0,
750 _t_ingest.as_secs_f64() * 1000.0,
751 );
752 }
753
754 {
755 let mut guard = self.db.salsa.write();
756 guard.rebuild_workspace_symbol_index();
757 }
758
759 crate::collector::print_collector_stats();
760 }
761}